Compare commits

...
Author SHA1 Message Date
Calum H. (IMB11) 6d50d7ebf9 fix: lint 2026-06-08 21:08:57 +01:00
Calum H. (IMB11) f969157089 fix: impl fork tauri updater plugin 2026-06-08 20:53:55 +01:00
Calum H. (IMB11) 0dcaa5c19e fix: fix width 2026-06-08 20:45:39 +01:00
Calum H. (IMB11) ebfc628df0 Merge branch 'main' of https://github.com/modrinth/code 2026-06-08 20:19:28 +01:00
Calum H. (IMB11) c4190bdb5b feat: updater ui change 2026-06-08 20:19:27 +01:00
Calum H.andGitHub 9404d46782 feat: release channels instance setting (#6252)
* feat: rough release channels impl draft

* feat: move to bottom + lint

* fix: invalidate content queries on channel change

* fix: change to chips

* fix: lint

* fix: copy
2026-06-08 17:10:59 +00:00
Calum H.andGitHub 926c72de42 fix: files tab drag and drop (#6325)
* fix: files drag drop

* fix: standardize drag and drop + fix files tab permissions
2026-06-08 17:03:30 +00:00
9729737d7d fix: replace save banner in env migration modal with physical bn (#6028)
* feat: dont use save banner for project env modal

* fix: modal btn gap

* fix: lint

* Update packages/ui/src/components/project/settings/environment/ProjectEnvironmentModal.vue

Signed-off-by: Calum H. <hendersoncal117@gmail.com>

* fix: lint

* fix: import

---------

Signed-off-by: Calum H. <hendersoncal117@gmail.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-06-08 16:52:35 +00:00
Truman GaoandGitHub 97a8c11b50 fix: incorrectly showing empty state for versions page (#6303)
* fix: incorrectly showing empty state for versions page

* refactor: pnpm prepr
2026-06-08 16:40:00 +00:00
Truman GaoandGitHub 4d654162eb refactor: rename query-filter.ts to query-filter-utils.ts (#6335) 2026-06-08 16:39:12 +00:00
Modrinth BotandGitHub 33b1419bdf New translations from Crowdin (main) (#6332) 2026-06-08 16:20:46 +00:00
Calum H.andGitHub c28ba2e6a4 fix: z index of console tab expanded (#6334)
fix: z index
2026-06-08 15:56:14 +00:00
aecsocketandGitHub c7ba6ba8b2 Bump frontend version upload timeout (#6333)
Bump upload timeout
2026-06-08 15:43:18 +00:00
Calum H.andGitHub 7366c32df3 feat: incompat modal improvement (#6256)
* feat: incompat modal improvement

* feat: use ContentUpdaterModal and remove IncompatibilityWarningModal

* fix: lint

* fix: lint
2026-06-05 15:56:05 +00:00
707e219ff8 feat: use multi select for moderation reports (#6312)
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-06-05 14:55:05 +00:00
Calum H.andGitHub dfe12d4ecb feat: server access post release QA (#6316)
* fix: clicking users in table in app takes you to blank page instead of website

* fix: wrong loader icon on server panel

* fix: surface var misalignment

* fix: password managers still detecting username field as something to autofill

* feat: show users on backupitem components

* feat: seperators for filter sections

* fix: lint + change remove -> revoke

* fix: copy

* feat: align copy
2026-06-05 14:54:27 +00:00
Truman GaoandGitHub c653228fe7 fix: malformed versions causing versions list page to crash (#6315) 2026-06-05 09:49:59 +00:00
Prospector 352a196795 update blog date 2026-06-04 11:56:52 -07:00
Calum H.andGitHub cef9b1efe5 fix: release workflow (#6311) 2026-06-04 09:31:41 -07:00
Calum H.andGitHub 7d6c54cff9 feat: changelog (#6310) 2026-06-04 18:01:38 +02:00
bd97ace974 feat: hosting access tab (#5995)
* feat: implement access tab with dummy data

* fix: spacing

* feat: qa

* feat: implement backend

* qa: qa pass

* feat: fix user "search"

* fix: lint

* feat: change to bitfield

* feat: fix fields

* fix: lint

* fix: lint

* feat: hook up api

* feat: fix permissions

* feat: audit log table event start

* feat: better mobile mode for audit log table

* feat: i18n

* feat: qa

* feat: enforce permissions

* feat: email template start

* feat: qa

* fix: tooltip bug

* feat: qa

* impl: sse support in api-client

* feat: sse impl

* fix: desync path

* feat: time frame picker from analytics

* feat: QA

* fix: spacing

* fix: permisison audit log entries

* fix: hosting manage page shared server detection

* fix: lint

* feat: qa + lint

* feat: audit log table sort by time

* feat: finish frontend panel stuff

* fix: lint

* fix: backend alignment

* fix: lint

* fix: supress friend errors

* feat: qa

* fix: qa

* fix: lint

* fix: utils barrel

* fix: safari cookies in dev

* fix: pin nuxt

* feat: fixes + notif fix

* fix: notifications

* feat: qa

* fix: notification sync not happening immediately

* fix: qa

* fix: qa

* feat: qa

* blog + prepr

* feat: toast shit

* blog images

* thumbnail update one last time

* prepr

* feat: use reinvite route

* update images

* fix: reinvite stuff

* fix: lint

* fix: alignment of save bar

* fix: notif sizing

* fix: split up access

* fix: lint

* fix: lint

* fix: link

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-06-04 15:58:01 +00:00
58ad58f958 feat: user search (#6302)
* feat: user search

* refactor: use sqlx macro

* refactor: better name for escaped query

* style: cleanup unused import

* chore: update sqlx query cache

* fix: tests

---------

Co-authored-by: sychic <47618543+Sychic@users.noreply.github.com>
2026-06-03 19:38:24 +00:00
Prospector d907083d83 changelog 2026-06-03 11:38:23 -07:00
Truman GaoandGitHub 8371ff641a fix: analytics post release bugs (#6291)
* fix: previous period data was included in the table

* fix: revenue displaying stale data when viewing it from different metric and grouped by 6 hour or 1 hour

* fix: remove staletime on analytics query so switching tabs does not refersh query

* feat: add monetization alert

* fix-small: missing space in tooltip

* fix: incorrect y-axis formatting for trailing decimal 0s

* fix: switching tabs resets table series selection due to other refetches

* fix: always show month first in chart tooltip

* fix: change all time start date to be project published date

* fix: increase length on project name column

* fix: unknown download source data points not showing for download source breakdown

* fix: double unknown for loader

* fix: no data on country labeling incorrectly as "Unknown" instead of "Other"

* fix: date picker number inputs showing arrows

* fix: stat card showing enormous percentage for prev period by switching it to absolute value difference after 1000%

* fix: decimal values for playtime being rounded badly, resulting in 0.04 becoming 0.0

* fix: chips having stroke

* refactor: pnpm prepr

* fix: spacing in annoucement link

* fix: legend scroll shadow on top of event tooltip
2026-06-03 18:27:31 +00:00
Prospector b1cd16f966 update ads.txt 2026-06-03 10:23:11 -07:00
Prospector 40a06921ea changelog 2026-06-03 07:00:46 -07:00
Aaron CorreaandGitHub a7dc063e08 fix: use correct organization route parameter (#6294) 2026-06-03 13:41:02 +00:00
ThatGravyBoatandGitHub 64b61d8fd0 chore: misc moderation changes (#6296)
* feat: report filter by target, issue type, project type

* fix: reply modal showing up for staff

* chore: change minimum class source from 10 to 2
2026-06-03 13:14:17 +00:00
5e7d4cc838 fix: notifs not live (#6299)
* fix: notifs not live

* Update apps/labrinth/src/routes/internal/external_notifications.rs

Co-authored-by: Sychic <47618543+Sychic@users.noreply.github.com>
Signed-off-by: Calum H. <hendersoncal117@gmail.com>

* fix: fmt

---------

Signed-off-by: Calum H. <hendersoncal117@gmail.com>
Co-authored-by: Sychic <47618543+Sychic@users.noreply.github.com>
2026-06-03 13:00:12 +00:00
ProspectorandGitHub a0c80b13a4 fix: no mention of Australia on hosting landing page (#6288) 2026-06-03 02:25:16 +00:00
Calum H.andGitHub 3c051f5b1d feat: add notifs onto friends ws temporarily (#6290)
* feat: add notifs onto friends ws temporarily

* fix: lint + styling

* fix: regressions
2026-06-02 19:47:37 +00:00
Calum H.andGitHub 940a796ba5 chore: changelog (#6286) 2026-06-02 18:39:29 +02:00
6ee5e4df19 feat: access labrinth backend (#6284)
* feat: redirect `/hosting` to archon

* feat: server invite notification type

* feat: direct email notification endpoint

* feat: revoke notification endpoint

* feat: specify users to remove notifications from

* refactor: insert notifications before sending emails

* refactor: rename endpoint

* refactor: remove archon redirect

* style: mark field unused

* feat: dedup external notifications

* feat: add server invite email templates

* style: remove unnecessary format

---------

Co-authored-by: sychic <47618543+Sychic@users.noreply.github.com>
2026-06-02 16:34:04 +00:00
aecsocketandGitHub d61397097c Tilitfy webhook changes (#6267)
* Tilitfy webhook changes

* add env

* fix
2026-06-02 15:14:02 +02:00
Calum H.andGitHub cfe45b368c fix: memory issues when importing giant mrpack files (#6278)
* feat: dont load mrpacks into memory if they are local imports

* fix: frontend
2026-06-02 12:59:41 +00:00
440 changed files with 41154 additions and 4083 deletions
+11 -4
View File
@@ -30,6 +30,10 @@ on:
- prod-with-staging-archon
default: prod
required: false
app-version-override:
description: Temporary app version override for updater testing
type: string
required: false
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -126,7 +130,10 @@ jobs:
- name: Set application version and environment
shell: bash
run: |
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
APP_VERSION="${{ inputs.app-version-override }}"
if [ -z "$APP_VERSION" ]; then
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
fi
BUILD_ENVIRONMENT="${{ inputs.environment || 'prod' }}"
echo "Setting application version to $APP_VERSION"
echo "Using environment $BUILD_ENVIRONMENT"
@@ -153,7 +160,7 @@ jobs:
fi
- name: Build macOS app
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }}
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }}
if: contains(matrix.platform, 'macos')
env:
TAURI_BUNDLER_DMG_IGNORE_CI: 'true'
@@ -168,7 +175,7 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: Build Linux app
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json' }}
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json' }}
if: contains(matrix.platform, 'ubuntu')
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
@@ -179,7 +186,7 @@ jobs:
[System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream
$env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD"
$env:JAVA_HOME = "$env:JAVA_HOME_17_X64"
${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles "nsis,updater"' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json --verbose --bundles "nsis,updater"' }}
${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles "nsis,updater"' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json --verbose --bundles "nsis,updater"' }}
Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue
if: contains(matrix.platform, 'windows')
env:
+4 -1
View File
@@ -57,8 +57,11 @@ jobs:
- name: Extract app changelog
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ env.VERSION_TAG }}
run: npx --yes tsx scripts/build-theseus-release-notes.ts
run: |
LAST_GITHUB_RELEASE_PUBLISHED_AT=$(gh api "repos/${{ github.repository }}/releases/latest" --jq '.published_at // ""' 2>/dev/null || true)
LAST_GITHUB_RELEASE_PUBLISHED_AT="$LAST_GITHUB_RELEASE_PUBLISHED_AT" npx --yes tsx scripts/build-theseus-release-notes.ts
- name: Generate version manifest
run: |
+1 -1
View File
@@ -9,7 +9,7 @@
"files.insertFinalNewline": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "always"
"source.organizeImports": "never"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[vue]": {
+1 -1
View File
@@ -64,7 +64,7 @@ The website and app `prepr` commands
Each project may have its own `CLAUDE.md` with detailed instructions:
- [`apps/labrinth/CLAUDE.md`](apps/labrinth/CLAUDE.md) — Backend API
- [`apps/labrinth/AGENTS.md`](apps/labrinth/AGENTS.md) — Backend API
- [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website
## Code Guidelines
Generated
+60 -19
View File
@@ -1378,7 +1378,7 @@ dependencies = [
"bitflags 2.9.4",
"cexpr",
"clang-sys",
"itertools 0.12.1",
"itertools 0.13.0",
"log",
"prettyplease",
"proc-macro2",
@@ -2927,7 +2927,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.7.4",
"libloading 0.8.8",
]
[[package]]
@@ -3260,7 +3260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -4577,7 +4577,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.5.10",
"socket2 0.6.1",
"system-configuration",
"tokio",
"tower-service",
@@ -4817,7 +4817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown 0.16.0",
"hashbrown 0.15.5",
"serde",
"serde_core",
]
@@ -4974,7 +4974,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.2",
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -5439,7 +5439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.53.5",
]
[[package]]
@@ -7701,7 +7701,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls 0.23.32",
"socket2 0.5.10",
"socket2 0.6.1",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -7738,9 +7738,9 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.5.10",
"socket2 0.6.1",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -8159,15 +8159,20 @@ dependencies = [
"http-body 1.0.1",
"http-body-util",
"hyper 1.7.0",
"hyper-rustls 0.27.7",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"rustls 0.23.32",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-rustls 0.26.4",
"tokio-util",
"tower",
"tower-http",
@@ -8413,7 +8418,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -8426,7 +8431,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -8509,6 +8514,33 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls 0.23.32",
"rustls-native-certs 0.8.1",
"rustls-platform-verifier-android",
"rustls-webpki 0.103.7",
"security-framework 3.5.1",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
@@ -9654,7 +9686,6 @@ dependencies = [
"cfg-if",
"libc",
"psm",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
@@ -10264,9 +10295,8 @@ dependencies = [
[[package]]
name = "tauri-plugin-updater"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b"
version = "2.10.1"
source = "git+https://github.com/modrinth/plugins-workspace?rev=0d30f2aa28ec668ce187d527da1c475da3c01cbc#0d30f2aa28ec668ce187d527da1c475da3c01cbc"
dependencies = [
"base64 0.22.1",
"dirs",
@@ -10278,7 +10308,8 @@ dependencies = [
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest 0.12.24",
"reqwest 0.13.2",
"rustls 0.23.32",
"semver",
"serde",
"serde_json",
@@ -10419,7 +10450,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix 1.1.2",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -10514,6 +10545,7 @@ dependencies = [
"flate2",
"fs4",
"futures",
"futures-lite 2.6.1",
"heck 0.5.0",
"hickory-resolver 0.25.2",
"indicatif",
@@ -11956,6 +11988,15 @@ dependencies = [
"libwebp-sys",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "0.26.11"
@@ -12077,7 +12118,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.61.2",
]
[[package]]
+2 -1
View File
@@ -85,6 +85,7 @@ eyre = "0.6.12"
flate2 = "1.1.4"
fs4 = { version = "0.13.1", default-features = false }
futures = "0.3.31"
futures-lite = "2.6.1"
futures-util = "0.3.31"
heck = "0.5.0"
hex = "0.4.3"
@@ -191,7 +192,7 @@ tauri-plugin-http = "2.5.7"
tauri-plugin-opener = "2.5.0"
tauri-plugin-os = "2.3.1"
tauri-plugin-single-instance = "2.3.4"
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
tauri-plugin-updater = { git = "https://github.com/modrinth/plugins-workspace", rev = "0d30f2aa28ec668ce187d527da1c475da3c01cbc", default-features = false, features = [
"rustls-tls",
"zip",
] }
+281 -118
View File
@@ -12,7 +12,6 @@ import {
ArrowBigUpDashIcon,
ChangeSkinIcon,
CompassIcon,
DownloadIcon,
ExternalIcon,
HomeIcon,
LeftArrowIcon,
@@ -36,6 +35,7 @@ import {
ButtonStyled,
commonMessages,
ContentInstallModal,
ContentUpdaterModal,
CreationFlowModal,
defineMessages,
I18nDebugPanel,
@@ -44,7 +44,6 @@ import {
NotificationPanel,
OverflowMenu,
PopupNotificationPanel,
ProgressSpinner,
provideModalBehavior,
provideModrinthClient,
provideNotificationManager,
@@ -75,7 +74,6 @@ import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import ErrorModal from '@/components/ui/ErrorModal.vue'
import FriendsList from '@/components/ui/friends/FriendsList.vue'
import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue'
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
import UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue'
import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue'
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
@@ -95,7 +93,7 @@ import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads
import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
import { check_reachable } from '@/helpers/auth.js'
import { get_user, get_version } from '@/helpers/cache.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { command_listener, notification_listener, warning_listener } from '@/helpers/events.js'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
import { create_profile_and_install_from_file } from '@/helpers/pack'
import { list } from '@/helpers/profile.js'
@@ -113,6 +111,16 @@ import {
setRestartAfterPendingUpdate,
} from '@/helpers/utils.js'
import i18n from '@/i18n.config'
import {
appUpdateState,
downloadAvailableAppUpdate,
getNextAppUpdatePopupTime,
installAvailableAppUpdate,
markAppUpdateActionable,
markAppUpdatePopupShown,
openAppUpdateChangelog,
setAppUpdateActions,
} from '@/providers/app-update.ts'
import { createContentInstall, provideContentInstall } from '@/providers/content-install'
import {
provideAppUpdateDownloadProgress,
@@ -241,6 +249,7 @@ const {
const news = ref([])
const availableSurvey = ref(false)
const displayedServerInviteNotifications = new Set()
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
@@ -296,6 +305,7 @@ onUnmounted(async () => {
document.querySelector('body').removeEventListener('click', handleClick)
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
unsubscribeSidebarToggle()
clearDelayedUpdatePopup()
await unlistenUpdateDownload?.()
})
@@ -312,18 +322,6 @@ const messages = defineMessages({
id: 'app.update.complete-toast.text',
defaultMessage: 'Click here to view the changelog.',
},
reloadToUpdate: {
id: 'app.update.reload-to-update',
defaultMessage: 'Reload to install update',
},
downloadUpdate: {
id: 'app.update.download-update',
defaultMessage: 'Download update',
},
downloadingUpdate: {
id: 'app.update.downloading-update',
defaultMessage: 'Downloading update ({percent}%)',
},
authUnreachableHeader: {
id: 'app.auth-servers.unreachable.header',
defaultMessage: 'Cannot reach authentication servers',
@@ -611,6 +609,16 @@ const {
handleModpackDuplicateCreateAnyway: handleContentInstallModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance: handleContentInstallModpackDuplicateGoToInstance,
setIncompatibilityWarningModal: setContentIncompatibilityWarningModal,
incompatibilityWarningVersions: contentInstallIncompatibilityWarningVersions,
incompatibilityWarningCurrentGameVersion: contentInstallIncompatibilityWarningCurrentGameVersion,
incompatibilityWarningCurrentLoader: contentInstallIncompatibilityWarningCurrentLoader,
incompatibilityWarningProjectType: contentInstallIncompatibilityWarningProjectType,
incompatibilityWarningProjectIconUrl: contentInstallIncompatibilityWarningProjectIconUrl,
incompatibilityWarningProjectName: contentInstallIncompatibilityWarningProjectName,
incompatibilityWarningMessage: contentInstallIncompatibilityWarningMessage,
incompatibilityWarningInstalling: contentInstallIncompatibilityWarningInstalling,
handleIncompatibilityWarningInstall: handleContentInstallIncompatibilityWarningInstall,
handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel,
} = contentInstall
const serverInstall = createServerInstall({ router, handleError, popupNotificationManager })
@@ -632,6 +640,12 @@ const updateToPlayModal = ref()
const modrinthLoginFlowWaitModal = ref()
watch(incompatibilityWarningModal, (modal) => {
if (modal) {
setContentIncompatibilityWarningModal(modal)
}
})
setupAuthProvider(credentials, async (_redirectPath) => {
await signIn()
})
@@ -752,6 +766,86 @@ const accounts = ref(null)
provide('accountsCard', accounts)
command_listener(handleCommand)
notification_listener(handleLiveNotification)
async function markLiveNotificationRead(notification) {
try {
await tauriApiClient.labrinth.notifications_v2.markAsRead(notification.id)
} catch (error) {
if (error instanceof ModrinthApiError && error.statusCode === 404) {
console.warn(`notification ${notification.id} could not be marked as read`, error)
return
}
throw error
}
}
async function respondToServerInvite(notification, action) {
const serverId = notification.body?.server_id
if (typeof serverId !== 'string') {
throw new Error('Missing server ID for invite notification.')
}
await tauriApiClient.request(`/servers/${serverId}/invites/${action}`, {
api: 'archon',
version: 1,
method: 'POST',
})
await markLiveNotificationRead(notification)
return serverId
}
async function acceptServerInviteNotification(notification) {
try {
const serverId = await respondToServerInvite(notification, 'accept')
await router.push(`/hosting/manage/${encodeURIComponent(serverId)}`)
queryClient.invalidateQueries({ queryKey: ['servers'] })
} catch (error) {
handleError(error)
}
}
async function declineServerInviteNotification(notification) {
try {
await respondToServerInvite(notification, 'decline')
} catch (error) {
handleError(error)
}
}
function openServerInviteInviterProfile(inviterName) {
if (!inviterName) return
openUrl(`${config.siteUrl}/user/${encodeURIComponent(inviterName)}`)
}
async function handleLiveNotification(notification) {
if (notification?.body?.type !== 'server_invite' || notification.read) return
if (displayedServerInviteNotifications.has(notification.id)) return
displayedServerInviteNotifications.add(notification.id)
const serverName =
typeof notification.body.server_name === 'string' ? notification.body.server_name : 'a server'
const inviterId = notification.body.invited_by
const invitedBy =
typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null
addPopupNotification({
title: serverName,
autoCloseMs: null,
toast: {
type: 'server-invite',
actorName: invitedBy?.username ?? null,
actorAvatarUrl: invitedBy?.avatar_url ?? null,
entityName: serverName,
onAccept: () => acceptServerInviteNotification(notification),
onDecline: () => declineServerInviteNotification(notification),
onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null),
},
})
}
async function handleCommand(e) {
if (!e) return
@@ -785,20 +879,21 @@ async function handleCommand(e) {
}
const appUpdateDownload = {
progress: ref(0),
progress: appUpdateState.progress,
version: ref(),
}
let unlistenUpdateDownload
const downloadProgress = computed(() => appUpdateDownload.progress.value)
const downloadPercent = computed(() => Math.trunc(appUpdateDownload.progress.value * 100))
const metered = ref(true)
const finishedDownloading = ref(false)
const restarting = ref(false)
const availableUpdate = ref(null)
const updateSize = ref(null)
const updatesEnabled = ref(true)
const {
metered,
finishedDownloading,
downloading,
restarting,
availableUpdate,
updateSize,
updatesEnabled,
} = appUpdateState
let delayedUpdatePopupTimeout = null
const updatePopupMessages = defineMessages({
updateAvailable: {
@@ -809,11 +904,6 @@ const updatePopupMessages = defineMessages({
id: 'app.update-popup.download-complete',
defaultMessage: 'Download complete',
},
body: {
id: 'app.update-popup.body',
defaultMessage:
'Modrinth App v{version} is ready to install! Reload to update now, or automatically when you close Modrinth App.',
},
meteredBody: {
id: 'app.update-popup.body.metered',
defaultMessage: `Modrinth App v{version} is available now! Since you're on a metered network, we didn't automatically download it.`,
@@ -829,7 +919,7 @@ const updatePopupMessages = defineMessages({
},
reload: {
id: 'app.update-popup.reload',
defaultMessage: 'Reload',
defaultMessage: 'Reload to update',
},
download: {
id: 'app.update-popup.download',
@@ -841,6 +931,106 @@ const updatePopupMessages = defineMessages({
},
})
function clearDelayedUpdatePopup() {
if (delayedUpdatePopupTimeout !== null) {
clearTimeout(delayedUpdatePopupTimeout)
delayedUpdatePopupTimeout = null
}
}
function getCurrentUpdatePromptStage() {
return finishedDownloading.value ? 'downloaded' : 'available'
}
function scheduleDelayedUpdatePopup() {
clearDelayedUpdatePopup()
const version = availableUpdate.value?.version
if (!version) {
return
}
const nextPopupTime = getNextAppUpdatePopupTime(version, getCurrentUpdatePromptStage())
if (nextPopupTime === null) {
return
}
const delay = nextPopupTime - Date.now()
if (delay <= 0) {
showDelayedUpdatePopup()
return
}
delayedUpdatePopupTimeout = setTimeout(showDelayedUpdatePopup, Math.min(delay, 2_147_483_647))
}
function showDelayedUpdatePopup() {
const update = availableUpdate.value
if (!update) {
return
}
const stage = getCurrentUpdatePromptStage()
const nextPopupTime = getNextAppUpdatePopupTime(update.version, stage)
if (nextPopupTime === null) {
return
}
if (Date.now() < nextPopupTime) {
scheduleDelayedUpdatePopup()
return
}
if (metered.value && !finishedDownloading.value) {
addPopupNotification({
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.meteredBody, { version: update.version }),
type: 'info',
autoCloseMs: null,
buttons: [
{
label: formatMessage(updatePopupMessages.download, {
size: formatBytes(updateSize.value ?? 0),
}),
action: () => downloadAvailableAppUpdate(),
color: 'brand',
},
{
label: formatMessage(updatePopupMessages.changelog),
action: () => openAppUpdateChangelog(),
keepOpen: true,
},
],
})
} else if (finishedDownloading.value) {
addPopupNotification({
title: formatMessage(updatePopupMessages.downloadComplete),
text: formatMessage(updatePopupMessages.downloadedBody, {
version: update.version,
}),
type: 'success',
autoCloseMs: null,
buttons: [
{
label: formatMessage(updatePopupMessages.reload),
action: () => installAvailableAppUpdate(),
color: 'brand',
},
{
label: formatMessage(updatePopupMessages.changelog),
action: () => openAppUpdateChangelog(),
keepOpen: true,
},
],
})
} else {
scheduleDelayedUpdatePopup()
return
}
markAppUpdatePopupShown(update.version, stage)
}
async function checkUpdates() {
if (!(await areUpdatesEnabled())) {
console.log('Skipping update check as updates are disabled in this build or environment')
@@ -864,11 +1054,15 @@ async function checkUpdates() {
if (isExistingUpdate) {
console.log('Update is already known')
scheduleDelayedUpdatePopup()
return
}
appUpdateDownload.progress.value = 0
finishedDownloading.value = false
downloading.value = false
updateSize.value = null
availableUpdate.value = update
console.log(`Update ${update.version} is available.`)
@@ -878,34 +1072,11 @@ async function checkUpdates() {
downloadUpdate(update)
} else {
console.log(`Metered connection detected, not auto-downloading update.`)
getUpdateSize(update.rid).then((size) => {
updateSize.value = size
addPopupNotification({
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.meteredBody, { version: update.version }),
type: 'info',
autoCloseMs: null,
buttons: [
{
label: formatMessage(updatePopupMessages.download, {
size: formatBytes(updateSize.value ?? 0),
}),
action: () => downloadAvailableUpdate(),
color: 'brand',
},
{
label: formatMessage(updatePopupMessages.changelog),
action: () => openUrl('https://modrinth.com/news/changelog?filter=app'),
keepOpen: true,
},
],
})
})
markAppUpdateActionable(update.version)
scheduleDelayedUpdatePopup()
}
getUpdateSize(update.rid).then((size) => (updateSize.value = size))
availableUpdate.value = update
}
await performCheck()
@@ -927,12 +1098,17 @@ async function checkLinuxUpdates() {
const latestVersion = updates?.version
if (latestVersion && latestVersion !== currentVersion) {
addPopupNotification({
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.linuxBody, { version: latestVersion }),
type: 'info',
autoCloseMs: null,
})
markAppUpdateActionable(latestVersion)
const nextPopupTime = getNextAppUpdatePopupTime(latestVersion)
if (nextPopupTime !== null && Date.now() >= nextPopupTime) {
addPopupNotification({
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.linuxBody, { version: latestVersion }),
type: 'info',
autoCloseMs: null,
})
markAppUpdatePopupShown(latestVersion)
}
}
} catch (e) {
console.error('Failed to check for updates:', e)
@@ -946,55 +1122,48 @@ async function downloadAvailableUpdate() {
async function downloadUpdate(versionToDownload) {
if (!versionToDownload) {
handleError(`Failed to download update: no version available`)
return
}
if (appUpdateDownload.progress.value !== 0) {
if (downloading.value || appUpdateDownload.progress.value !== 0) {
console.error(`Update ${versionToDownload.version} already downloading`)
return
}
console.log(`Downloading update ${versionToDownload.version}`)
downloading.value = true
try {
enqueueUpdateForInstallation(versionToDownload.rid).then(() => {
finishedDownloading.value = true
unlistenUpdateDownload?.().then(() => {
unlistenUpdateDownload = null
enqueueUpdateForInstallation(versionToDownload.rid)
.then(() => {
downloading.value = false
finishedDownloading.value = true
unlistenUpdateDownload?.().then(() => {
unlistenUpdateDownload = null
})
console.log('Finished downloading!')
markAppUpdateActionable(versionToDownload.version, 'downloaded')
scheduleDelayedUpdatePopup()
})
console.log('Finished downloading!')
addPopupNotification({
title: formatMessage(updatePopupMessages.downloadComplete),
text: formatMessage(updatePopupMessages.downloadedBody, {
version: versionToDownload.version,
}),
type: 'success',
autoCloseMs: null,
buttons: [
{
label: formatMessage(updatePopupMessages.reload),
action: () => installUpdate(),
color: 'brand',
},
{
label: formatMessage(updatePopupMessages.changelog),
action: () => openUrl('https://modrinth.com/news/changelog?filter=app'),
keepOpen: true,
},
],
.catch((e) => {
downloading.value = false
appUpdateDownload.progress.value = 0
handleError(e)
})
})
unlistenUpdateDownload = await subscribeToDownloadProgress(
appUpdateDownload,
versionToDownload.version,
)
} catch (e) {
downloading.value = false
appUpdateDownload.progress.value = 0
handleError(e)
}
}
async function installUpdate() {
restarting.value = true
try {
await setRestartAfterPendingUpdate(true)
} catch (e) {
@@ -1007,6 +1176,12 @@ async function installUpdate() {
}, 250)
}
setAppUpdateActions({
download: downloadAvailableUpdate,
install: installUpdate,
changelog: () => openUrl('https://modrinth.com/news/changelog?filter=app'),
})
async function openModrinthProjectLinkInApp(parsed) {
const { slug, pathSuffix, url } = parsed
const loadToken = loading.begin()
@@ -1276,33 +1451,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
<PlusIcon />
</NavButton>
<div class="flex flex-grow"></div>
<Transition name="nav-button-animated">
<div v-if="availableUpdate && !restarting && (finishedDownloading || metered)">
<NavButton
v-tooltip.right="
formatMessage(
finishedDownloading
? messages.reloadToUpdate
: downloadProgress === 0
? messages.downloadUpdate
: messages.downloadingUpdate,
{
percent: downloadPercent,
},
)
"
:to="finishedDownloading ? installUpdate : downloadAvailableUpdate"
>
<ProgressSpinner
v-if="downloadProgress > 0 && downloadProgress < 1"
class="text-brand"
:progress="downloadProgress"
/>
<RefreshCwIcon v-else-if="finishedDownloading" class="text-brand" />
<DownloadIcon v-else class="text-brand" />
</NavButton>
</div>
</Transition>
<NavButton
v-tooltip.right="formatMessage(commonMessages.settingsLabel)"
:to="() => $refs.settingsModal.show()"
@@ -1550,7 +1698,22 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@go-to-instance="handleModpackDuplicateGoToInstance"
/>
<AddServerToInstanceModal ref="addServerToInstanceModal" />
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
<ContentUpdaterModal
ref="incompatibilityWarningModal"
mode="incompatibility-warning"
:versions="contentInstallIncompatibilityWarningVersions"
:current-game-version="contentInstallIncompatibilityWarningCurrentGameVersion"
:current-loader="contentInstallIncompatibilityWarningCurrentLoader"
current-version-id=""
:is-app="true"
:project-type="contentInstallIncompatibilityWarningProjectType"
:project-icon-url="contentInstallIncompatibilityWarningProjectIconUrl"
:project-name="contentInstallIncompatibilityWarningProjectName"
:warning="contentInstallIncompatibilityWarningMessage"
:action-loading="contentInstallIncompatibilityWarningInstalling"
@update="handleContentInstallIncompatibilityWarningInstall"
@cancel="handleContentInstallIncompatibilityWarningCancel"
/>
<ModpackAlreadyInstalledModal
ref="contentInstallModpackAlreadyInstalledModal"
@create-anyway="handleContentInstallModpackDuplicateCreateAnyway"
@@ -1,5 +1,5 @@
<template>
<div class="flex gap-4 items-center">
<div class="flex gap-2 items-center">
<ButtonStyled
v-if="hasActiveLoadingBars && !hasVisibleActiveDownloadToasts"
color="brand"
@@ -14,6 +14,31 @@
<UnplugIcon class="text-secondary" />
<span class="text-sm text-contrast"> {{ formatMessage(messages.offline) }} </span>
</div>
<ButtonStyled color="brand" type="outlined" hover-color-fill="background">
<button
v-if="showUpdatePill"
type="button"
class="!h-[34px] overflow-hidden text-sm !transition-[width,opacity,transform,background-color,color,filter] !duration-200 ease-out"
:class="[
updatePillWidthClass,
{
'update-pill-ready-hidden': finishedDownloading && !animateReadyPill,
'update-pill-ready-visible': finishedDownloading && animateReadyPill,
},
]"
:disabled="isUpdateDownloading"
:aria-busy="isUpdateDownloading"
@click="handleUpdateClick"
>
<RefreshCwIcon v-if="finishedDownloading" :class="{ 'animate-spin': restarting }" />
<DownloadIcon v-else />
<span v-if="isUpdateDownloading">
{{ formatMessage(messages.downloadingUpdate) }}
<span class="inline-block w-[3ch] text-right tabular-nums">{{ downloadPercent }}%</span>
</span>
<span v-else>{{ updateLabel }}</span>
</button>
</ButtonStyled>
<div
class="flex border-solid border-surface-5 text-sm items-center gap-2 py-1.5 px-3 rounded-xl border"
>
@@ -119,6 +144,7 @@ import {
DownloadIcon,
DropdownIcon,
OnlineIndicatorIcon,
RefreshCwIcon,
StarIcon,
StopCircleIcon,
TerminalSquareIcon,
@@ -133,8 +159,9 @@ import {
type PopupNotificationProgressItem,
useVIntl,
} from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { Dropdown } from 'floating-vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { trackEvent } from '@/helpers/analytics'
@@ -144,6 +171,11 @@ import { get_many as getInstances } from '@/helpers/profile.js'
import type { LoadingBar } from '@/helpers/state'
import { progress_bars_list } from '@/helpers/state'
import type { GameInstance } from '@/helpers/types'
import {
appUpdateState,
downloadAvailableAppUpdate,
installAvailableAppUpdate,
} from '@/providers/app-update'
const { handleError } = injectNotificationManager()
const popupNotificationManager = injectPopupNotificationManager()
@@ -208,8 +240,96 @@ const messages = defineMessages({
id: 'app.action-bar.view-active-downloads',
defaultMessage: 'View active downloads',
},
update: {
id: 'app.action-bar.update',
defaultMessage: 'Update',
},
downloadingUpdate: {
id: 'app.action-bar.downloading-update',
defaultMessage: 'Downloading update',
},
reloadToUpdate: {
id: 'app.action-bar.reload-to-update',
defaultMessage: 'Reload to update',
},
})
const {
downloading,
downloadPercent,
downloadProgress,
finishedDownloading,
isVisible: isUpdateVisible,
metered,
restarting,
} = appUpdateState
const isUpdateDownloading = computed(
() =>
downloading.value ||
(downloadProgress.value > 0 && downloadProgress.value < 1 && !finishedDownloading.value),
)
const showUpdatePill = computed(
() => isUpdateVisible.value && (finishedDownloading.value || metered.value),
)
const animateReadyPill = ref(false)
const updateLabel = computed(() => {
if (isUpdateDownloading.value) {
return formatMessage(messages.downloadingUpdate)
}
if (finishedDownloading.value) {
return formatMessage(messages.reloadToUpdate)
}
return formatMessage(messages.update)
})
const updatePillWidthClass = computed(() => {
if (isUpdateDownloading.value) {
return 'w-[219px]'
}
if (finishedDownloading.value) {
return 'w-[166px]'
}
return '!w-[96px]'
})
let readyPillAnimationFrame: number | null = null
watch([showUpdatePill, finishedDownloading], async ([show, ready], [wasShown, wasReady]) => {
if (readyPillAnimationFrame !== null) {
cancelAnimationFrame(readyPillAnimationFrame)
readyPillAnimationFrame = null
}
if (!show || !ready) {
animateReadyPill.value = false
return
}
if (wasShown && wasReady) {
return
}
animateReadyPill.value = false
await nextTick()
readyPillAnimationFrame = requestAnimationFrame(() => {
animateReadyPill.value = true
readyPillAnimationFrame = null
})
})
async function handleUpdateClick() {
if (isUpdateDownloading.value) {
return
}
if (finishedDownloading.value) {
await installAvailableAppUpdate()
} else {
await downloadAvailableAppUpdate()
}
}
const currentProcesses = ref<RunningProcess[]>([])
const selectedProcess = ref<RunningProcess | undefined>()
@@ -284,6 +404,7 @@ function goToTerminal(path?: string) {
}
const currentLoadingBars = ref<LoadingBar[]>([])
const currentLoadingBarIconUrls = ref<Record<string, string | null>>({})
const notificationId = ref<string | number | null>(null)
const dismissed = ref(false)
@@ -303,6 +424,16 @@ function getLoadingText(loadingBar: LoadingBar): string {
return loadingBar.message ? `${percent}% ${loadingBar.message}` : `${percent}%`
}
function getDisplayIconUrl(icon: string | null | undefined): string | null {
if (!icon) {
return null
}
if (/^(https?:|data:|blob:|asset:|tauri:)/.test(icon)) {
return icon
}
return convertFileSrc(icon)
}
function getNotification(): PopupNotification | null {
if (!notificationId.value) {
return null
@@ -326,6 +457,7 @@ function buildDownloadItems(): PopupNotificationProgressItem[] {
id: getLoadingBarKey(bar),
title: bar.title ?? '',
text: getLoadingText(bar),
iconUrl: currentLoadingBarIconUrls.value[getLoadingBarKey(bar)] ?? null,
progress: getLoadingProgress(bar),
waiting: !bar.total || bar.total <= 0,
}))
@@ -400,6 +532,32 @@ async function refreshLoadingBars() {
.map(formatLoadingBars)
.filter((bar) => bar?.bar_type?.type !== 'launcher_update')
const profilePaths = Array.from(
new Set(
currentLoadingBars.value
.map((bar) => bar.bar_type?.profile_path)
.filter((path): path is string => !!path),
),
)
const profiles = profilePaths.length
? await getInstances(profilePaths).catch((error) => {
handleError(error)
return []
})
: []
const profileIconUrls = new Map(
profiles.map((profile) => [profile.path, getDisplayIconUrl(profile.icon_path)]),
)
currentLoadingBarIconUrls.value = Object.fromEntries(
currentLoadingBars.value.map((bar) => {
const barIconUrl = getDisplayIconUrl(bar.bar_type?.icon)
const profileIconUrl = bar.bar_type?.profile_path
? profileIconUrls.get(bar.bar_type.profile_path)
: null
return [getLoadingBarKey(bar), barIconUrl ?? profileIconUrl ?? null]
}),
)
currentLoadingBars.value.sort((a, b) => {
const aKey = `${a.loading_bar_uuid ?? a.id ?? ''}`
const bKey = `${b.loading_bar_uuid ?? b.id ?? ''}`
@@ -430,5 +588,20 @@ onBeforeUnmount(() => {
window.removeEventListener('online', handleOnline)
unlistenProcess()
unlistenLoading()
if (readyPillAnimationFrame !== null) {
cancelAnimationFrame(readyPillAnimationFrame)
}
})
</script>
<style scoped>
.update-pill-ready-hidden {
opacity: 0;
transform: scale(0.96);
}
.update-pill-ready-visible {
opacity: 1;
transform: scale(1);
}
</style>
@@ -1,185 +0,0 @@
<template>
<ModalWrapper ref="incompatibleModal" header="Incompatibility warning" :on-hide="onInstall">
<div class="modal-body">
<p>
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
you're trying to install it on. Are you sure you want to continue? Dependencies will not be
installed.
</p>
<table>
<thead>
<tr class="header">
<th>{{ instance?.name }}</th>
<th>{{ project.title }}</th>
</tr>
</thead>
<tbody>
<tr class="content">
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
<td>
<Combobox
v-if="versions?.length > 1"
v-model="selectedVersionId"
:options="versionOptions"
:searchable="true"
placeholder="Select version"
force-direction="up"
:max-height="150"
/>
<span v-else>
<span>{{ selectedVersionLabel }}</span>
</span>
</td>
</tr>
</tbody>
</table>
<div class="button-group">
<ButtonStyled type="outlined">
<button @click="() => incompatibleModal.hide()"><XIcon />Cancel</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button :disabled="installing" @click="install()">
<DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}
</button>
</ButtonStyled>
</div>
</div>
</ModalWrapper>
</template>
<script setup>
import { DownloadIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
Combobox,
formatLoader,
injectNotificationManager,
useVIntl,
} from '@modrinth/ui'
import { computed, ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { add_project_from_version as installMod } from '@/helpers/profile'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const instance = ref(null)
const project = ref(null)
const versions = ref(null)
const selectedVersion = ref(null)
const incompatibleModal = ref(null)
const installing = ref(false)
const onInstall = ref(() => {})
const selectedVersionLabel = computed(() => {
if (!selectedVersion.value) return ''
return `${selectedVersion.value.name} (${selectedVersion.value.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')} - ${selectedVersion.value.game_versions.join(', ')})`
})
const versionOptions = computed(() =>
(versions.value ?? []).map((version) => ({
value: version.id,
label: `${version.name} (${version.loaders
.map((name) => formatLoader(formatMessage, name))
.join(', ')} - ${version.game_versions.join(', ')})`,
})),
)
const selectedVersionId = computed({
get: () => selectedVersion.value?.id ?? null,
set: (value) => {
if (!value) return
selectedVersion.value = (versions.value ?? []).find((version) => version.id === value) ?? null
},
})
defineExpose({
show: (instanceVal, projectVal, projectVersions, selected, callback) => {
instance.value = instanceVal
versions.value = projectVersions ?? []
selectedVersion.value = selected ?? projectVersions?.[0] ?? null
project.value = projectVal
onInstall.value = callback
installing.value = false
incompatibleModal.value.show()
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
},
})
const install = async () => {
installing.value = true
await installMod(instance.value.path, selectedVersion.value.id, 'standalone').catch(handleError)
installing.value = false
onInstall.value(selectedVersion.value.id)
incompatibleModal.value.hide()
trackEvent('ProjectInstall', {
loader: instance.value.loader,
game_version: instance.value.game_version,
id: project.value,
version_id: selectedVersion.value.id,
project_type: project.value.project_type,
title: project.value.title,
source: 'ProjectIncompatibilityWarningModal',
})
}
</script>
<style lang="scss" scoped>
.data {
text-transform: capitalize;
}
table {
width: 100%;
border-radius: var(--radius-lg);
border-collapse: collapse;
box-shadow: 0 0 0 1px var(--color-button-bg);
}
th {
text-align: left;
padding: 1rem;
background-color: var(--color-bg);
overflow: hidden;
border-bottom: 1px solid var(--color-button-bg);
}
th:first-child {
border-top-left-radius: var(--radius-lg);
border-right: 1px solid var(--color-button-bg);
}
th:last-child {
border-top-right-radius: var(--radius-lg);
}
td {
padding: 1rem;
}
td:first-child {
border-right: 1px solid var(--color-button-bg);
}
.button-group {
display: flex;
justify-content: flex-end;
gap: 1rem;
}
.modal-body {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>
@@ -4,12 +4,14 @@ import {
Avatar,
ButtonStyled,
Checkbox,
Chips,
defineMessages,
injectNotificationManager,
OverflowMenu,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-dialog'
import { computed, type Ref, ref, watch } from 'vue'
@@ -25,14 +27,22 @@ import type { GameInstance } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const router = useRouter()
const queryClient = useQueryClient()
const deleteConfirmModal = ref()
const { instance } = injectInstanceSettings()
type ReleaseChannel = GameInstance['preferred_update_channel']
const releaseChannelOptions: ReleaseChannel[] = ['release', 'beta', 'alpha']
const title = ref(instance.value.name)
const icon: Ref<string | undefined> = ref(instance.value.icon_path)
const groups = ref([...instance.value.groups])
const savingReleaseChannel = ref(false)
const selectedReleaseChannel = ref<ReleaseChannel>(instance.value.preferred_update_channel)
const releaseChannelDisabledItems = computed<ReleaseChannel[]>(() =>
savingReleaseChannel.value ? [...releaseChannelOptions] : [],
)
const newCategoryInput = ref('')
@@ -51,6 +61,52 @@ const availableGroups = computed(() => [
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
])
function formatReleaseChannelLabel(channel: ReleaseChannel) {
switch (channel) {
case 'release':
return formatMessage(messages.updateChannelRelease)
case 'beta':
return formatMessage(messages.updateChannelBeta)
case 'alpha':
return formatMessage(messages.updateChannelAlpha)
}
}
function formatReleaseChannelDescription(channel: ReleaseChannel) {
switch (channel) {
case 'release':
return formatMessage(messages.updateChannelReleaseDescription)
case 'beta':
return formatMessage(messages.updateChannelBetaDescription)
case 'alpha':
return formatMessage(messages.updateChannelAlphaDescription)
}
}
watch(
() => [instance.value.path, instance.value.preferred_update_channel] as const,
() => {
if (!savingReleaseChannel.value) {
selectedReleaseChannel.value = instance.value.preferred_update_channel
}
},
)
watch(selectedReleaseChannel, async (channel, previousChannel) => {
const previousReleaseChannel = previousChannel ?? instance.value.preferred_update_channel
if (channel === instance.value.preferred_update_channel) return
savingReleaseChannel.value = true
const profilePath = instance.value.path
await edit(profilePath, { preferred_update_channel: channel })
.then(() => queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', profilePath] }))
.catch((error) => {
selectedReleaseChannel.value = previousReleaseChannel
handleError(error)
})
savingReleaseChannel.value = false
})
async function resetIcon() {
icon.value = undefined
await edit_icon(instance.value.path, null).catch(handleError)
@@ -175,6 +231,38 @@ const messages = defineMessages({
id: 'instance.settings.tabs.general.duplicate-button',
defaultMessage: 'Duplicate',
},
updateChannel: {
id: 'instance.settings.tabs.general.update-channel',
defaultMessage: 'Update channel',
},
updateChannelReleaseDescription: {
id: 'instance.settings.tabs.general.update-channel.release.description',
defaultMessage: 'Only release versions will be shown as available updates.',
},
updateChannelBetaDescription: {
id: 'instance.settings.tabs.general.update-channel.beta.description',
defaultMessage: 'Release and beta versions will be shown as available updates.',
},
updateChannelAlphaDescription: {
id: 'instance.settings.tabs.general.update-channel.alpha.description',
defaultMessage: 'Release, beta, and alpha versions will be shown as available updates.',
},
updateChannelRelease: {
id: 'instance.settings.tabs.general.update-channel.release',
defaultMessage: 'Release',
},
updateChannelBeta: {
id: 'instance.settings.tabs.general.update-channel.beta',
defaultMessage: 'Beta',
},
updateChannelAlpha: {
id: 'instance.settings.tabs.general.update-channel.alpha',
defaultMessage: 'Alpha',
},
selectUpdateChannelAriaLabel: {
id: 'instance.settings.tabs.general.update-channel.select',
defaultMessage: 'Select update channel',
},
deleteInstance: {
id: 'instance.settings.tabs.general.delete',
defaultMessage: 'Delete instance',
@@ -304,6 +392,23 @@ const messages = defineMessages({
</p>
</div>
<div class="flex flex-col gap-2.5 mt-6">
<h2 class="m-0 text-lg font-semibold text-contrast block">
{{ formatMessage(messages.updateChannel) }}
</h2>
<Chips
v-model="selectedReleaseChannel"
:items="releaseChannelOptions"
:format-label="formatReleaseChannelLabel"
:capitalize="false"
:disabled-items="releaseChannelDisabledItems"
:aria-label="formatMessage(messages.selectUpdateChannelAriaLabel)"
/>
<p class="m-0">
{{ formatReleaseChannelDescription(selectedReleaseChannel) }}
</p>
</div>
<div class="flex flex-col gap-2.5 mt-6">
<h2 id="delete-instance-label" class="m-0 text-lg font-semibold text-contrast block">
{{ formatMessage(messages.deleteInstance) }}
@@ -8,6 +8,7 @@ import {
InstallationSettingsLayout,
provideAppBackup,
provideInstallationSettings,
useDebugLogger,
useVIntl,
} from '@modrinth/ui'
import type { GameVersionTag, PlatformTag } from '@modrinth/utils'
@@ -34,9 +35,17 @@ import type { Manifest } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const debug = useDebugLogger('AppInstallationSettings')
const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings()
debug('metadata load: start', {
instancePath: instance.value.path,
loader: instance.value.loader,
gameVersion: instance.value.game_version,
installStage: instance.value.install_stage,
})
const [
fabric_versions,
forge_versions,
@@ -72,6 +81,15 @@ const [
.catch(handleError),
])
debug('metadata load: done', {
hasFabricManifest: !!fabric_versions?.value,
hasForgeManifest: !!forge_versions?.value,
hasQuiltManifest: !!quilt_versions?.value,
hasNeoforgeManifest: !!neoforge_versions?.value,
gameVersions: all_game_versions?.value?.length ?? 0,
availablePlatforms: loaders?.value?.map((loader) => loader.name) ?? [],
})
const { data: modpackInfo } = useQuery({
queryKey: computed(() => ['linkedModpackInfo', instance.value.path]),
queryFn: () => get_linked_modpack_info(instance.value.path, 'must_revalidate'),
@@ -95,11 +113,21 @@ function getManifest(loader: string) {
quilt: quilt_versions,
neoforge: neoforge_versions,
}
return map[loader]
const manifest = map[loader]
debug('getManifest:', {
loader,
hasManifest: !!manifest?.value,
gameVersions: manifest?.value?.gameVersions?.length ?? 0,
})
return manifest
}
provideAppBackup({
async createBackup() {
debug('createBackup: start', {
instancePath: instance.value.path,
instanceName: instance.value.name,
})
const allProfiles = await list()
const prefix = `${instance.value.name} - Backup #`
const existingNums = allProfiles
@@ -109,6 +137,7 @@ provideAppBackup({
const nextNum = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1
const newPath = await duplicate(instance.value.path)
await edit(newPath, { name: `${prefix}${nextNum}` })
debug('createBackup: done', { newPath, backupName: `${prefix}${nextNum}` })
},
})
@@ -165,32 +194,72 @@ provideInstallationSettings({
const manifest = getManifest(loader)
return !!manifest?.value?.gameVersions?.some((x) => item.version === x.id)
})
return (showSnapshots ? filtered : filtered.filter((x) => x.version_type === 'release')).map(
(x) => ({ value: x.version, label: x.version }),
)
const result = (
showSnapshots ? filtered : filtered.filter((x) => x.version_type === 'release')
).map((x) => ({ value: x.version, label: x.version }))
debug('resolveGameVersions:', {
loader,
showSnapshots,
totalVersions: versions.length,
filteredVersions: filtered.length,
resultVersions: result.length,
})
return result
},
resolveLoaderVersions(loader, gameVersion) {
if (loader === 'vanilla' || !gameVersion) return []
const manifest = getManifest(loader)
if (!manifest?.value) return []
if (loader === 'fabric' || loader === 'quilt') {
return manifest.value.gameVersions[0]?.loaders ?? []
if (loader === 'vanilla' || !gameVersion) {
debug('resolveLoaderVersions: skipped', { loader, gameVersion })
return []
}
return manifest.value.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? []
const manifest = getManifest(loader)
if (!manifest?.value) {
debug('resolveLoaderVersions: no manifest', { loader, gameVersion })
return []
}
if (loader === 'fabric' || loader === 'quilt') {
const result = manifest.value.gameVersions[0]?.loaders ?? []
debug('resolveLoaderVersions: fabric/quilt result', {
loader,
gameVersion,
count: result.length,
})
return result
}
const result =
manifest.value.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? []
debug('resolveLoaderVersions: result', { loader, gameVersion, count: result.length })
return result
},
resolveHasSnapshots(loader) {
const versions = all_game_versions?.value ?? []
if (loader === 'vanilla') return versions.some((x) => x.version_type !== 'release')
if (loader === 'vanilla') {
const result = versions.some((x) => x.version_type !== 'release')
debug('resolveHasSnapshots: vanilla', { loader, result })
return result
}
const manifest = getManifest(loader)
const supported = versions.filter(
(item) => !!manifest?.value?.gameVersions?.some((x) => item.version === x.id),
)
return supported.some((x) => x.version_type !== 'release')
const result = supported.some((x) => x.version_type !== 'release')
debug('resolveHasSnapshots:', {
loader,
totalVersions: versions.length,
supportedVersions: supported.length,
result,
})
return result
},
async save(platform, gameVersion, loaderVersionId) {
debug('save: called', {
instancePath: instance.value.path,
platform,
gameVersion,
loaderVersionId,
})
const editProfile: Record<string, string | undefined> = {
loader: platform,
game_version: gameVersion,
@@ -199,17 +268,21 @@ provideInstallationSettings({
editProfile.loader_version = loaderVersionId
}
await edit(instance.value.path, editProfile).catch(handleError)
debug('save: edit complete', { editProfile })
},
afterSave: async () => {
debug('afterSave: installing', { instancePath: instance.value.path })
await install(instance.value.path, false).catch(handleError)
trackEvent('InstanceRepair', {
loader: instance.value.loader,
game_version: instance.value.game_version,
})
debug('afterSave: done')
},
async repair() {
debug('repair: called', { instancePath: instance.value.path })
repairing.value = true
await install(instance.value.path, true).catch(handleError)
repairing.value = false
@@ -217,9 +290,11 @@ provideInstallationSettings({
loader: instance.value.loader,
game_version: instance.value.game_version,
})
debug('repair: done')
},
async reinstallModpack() {
debug('reinstallModpack: called', { instancePath: instance.value.path })
reinstalling.value = true
await update_repair_modrinth(instance.value.path).catch(handleError)
reinstalling.value = false
@@ -227,9 +302,11 @@ provideInstallationSettings({
loader: instance.value.loader,
game_version: instance.value.game_version,
})
debug('reinstallModpack: done')
},
async unlinkModpack() {
debug('unlinkModpack: called', { instancePath: instance.value.path })
await edit(instance.value.path, {
linked_data: null as unknown as undefined,
})
@@ -237,27 +314,38 @@ provideInstallationSettings({
queryKey: ['linkedModpackInfo', instance.value.path],
})
onUnlinked()
debug('unlinkModpack: done')
},
getCachedModpackVersions: () => null,
async fetchModpackVersions() {
debug('fetchModpackVersions: called', {
projectId: instance.value.linked_data?.project_id,
})
const versions = await get_project_versions(instance.value.linked_data!.project_id!).catch(
handleError,
)
debug('fetchModpackVersions: done', { count: versions?.length ?? 0 })
return (versions ?? []) as Labrinth.Versions.v2.Version[]
},
async getVersionChangelog(versionId: string) {
debug('getVersionChangelog: called', { versionId })
return (await get_version(versionId, 'must_revalidate').catch(
() => null,
)) as Labrinth.Versions.v2.Version | null
},
async onModpackVersionConfirm(version) {
debug('onModpackVersionConfirm: called', {
versionId: version.id,
instancePath: instance.value.path,
})
await update_managed_modrinth_version(instance.value.path, version.id)
await queryClient.invalidateQueries({
queryKey: ['linkedModpackInfo', instance.value.path],
})
debug('onModpackVersionConfirm: done')
},
updaterModalProps: computed(() => ({
+4
View File
@@ -98,6 +98,10 @@ export async function friend_listener(callback) {
return await listen('friend', (event) => callback(event.payload))
}
export async function notification_listener(callback) {
return await listen('notification', (event) => callback(event.payload))
}
/// Payload for the 'log' event
/*
LogPayload {
+1
View File
@@ -10,6 +10,7 @@ export interface LoadingBarType {
version?: string
profile_path?: string
pack_name?: string
icon?: string | null
}
export interface LoadingBar {
+3
View File
@@ -14,6 +14,7 @@ export type GameInstance = {
groups: string[]
linked_data?: LinkedData
preferred_update_channel: ReleaseChannel
created: Date
modified: Date
@@ -46,6 +47,8 @@ type LinkedData = {
locked: boolean
}
type ReleaseChannel = 'release' | 'beta' | 'alpha'
export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
type ContentFile = {
@@ -236,9 +236,6 @@
"app.settings.tabs.resource-management": {
"message": "إدارة الموارد"
},
"app.update-popup.body": {
"message": "تطبيق Modrinth v{version} جاهز للتثبيت! أعد التحميل للتحديث الآن، أو تلقائيًا عند إغلاق تطبيق Modrinth."
},
"app.update-popup.body.download-complete": {
"message": "انتهى تنزيل تطبيق Modrinth v{version}. أعد التحميل للتحديث الآن، أو تلقائيًا عند إغلاق تطبيق Modrinth."
},
@@ -269,15 +266,6 @@
"app.update.complete-toast.title": {
"message": "تم تثبيت الإصدار {version} بنجاح!"
},
"app.update.download-update": {
"message": "تنزيل التحديث"
},
"app.update.downloading-update": {
"message": "جار تنزيل التحديث ({percent}٪)"
},
"app.update.reload-to-update": {
"message": "أعد التحميل لتثبيت التحديث"
},
"app.world.server-modal.placeholder-address": {
"message": "مثال.مودرنث.جج"
},
+32 -11
View File
@@ -86,6 +86,9 @@
"app.appearance-settings.select-option": {
"message": "Vyberte možnost"
},
"app.appearance-settings.show-play-time.title": {
"message": "Ukázat herní čas"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Umožňí přepínat zobrazení postranního panelu."
},
@@ -326,8 +329,35 @@
"app.settings.tabs.resource-management": {
"message": "Správa zdrojů"
},
"app.update-popup.body": {
"message": "Aplikace Modrinth v{version} je připravena k instalaci! Nainstalujte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
"app.skins.add-button": {
"message": "Přidat skin"
},
"app.skins.apply-button": {
"message": "Použít"
},
"app.skins.delete-button": {
"message": "Smazat skin"
},
"app.skins.edit-button": {
"message": "Upravit skin"
},
"app.skins.modal.add-skin-button": {
"message": "Přidat skin"
},
"app.skins.preview.edit-button": {
"message": "Upravit skin"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.saved-skins": {
"message": "Uložené skiny"
},
"app.update-popup.body.download-complete": {
"message": "Stahování aplikace Modrinth v{version} bylo dokončeno. Nainstalujte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
@@ -359,15 +389,6 @@
"app.update.complete-toast.title": {
"message": "Verze {version} byla úspěšně nainstalována!"
},
"app.update.download-update": {
"message": "Stáhnout aktualizaci"
},
"app.update.downloading-update": {
"message": "Stahování aktualizace ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Restartovat aplikaci pro nainstalování aktualizace"
},
"app.world.server-modal.placeholder-address": {
"message": "priklad.modrinth.gg"
},
+152 -8
View File
@@ -80,6 +80,12 @@
"app.appearance-settings.select-option": {
"message": "Vælg en mulighed"
},
"app.appearance-settings.show-play-time.description": {
"message": "Vis hvor meget tid du har brugt på at spille en instance."
},
"app.appearance-settings.show-play-time.title": {
"message": "Vis tid spillet"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Aktivere muligheden for at slå sidepanelet til og fra."
},
@@ -359,8 +365,95 @@
"app.skins.modal.cape-fallback-name": {
"message": "Kappe"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} er klar til at blive installeret! Genindlæs for at opdatere nu, eller automatisk når du lukker Modrinth App."
"app.skins.modal.cape-section": {
"message": "Kappe"
},
"app.skins.modal.edit-title": {
"message": "Rediger skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Rediger skinnet først!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Ingen kappe"
},
"app.skins.modal.none-cape-option": {
"message": "Ingen"
},
"app.skins.modal.save-skin-button": {
"message": "Gem skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Gemmer..."
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Upload et skin først!"
},
"app.skins.preview.edit-button": {
"message": "Rediger skin"
},
"app.skins.previewing-badge": {
"message": "Forhåndsvisning"
},
"app.skins.rate-limit.text": {
"message": "Du skifter dit skin for ofte. Mojang's servere har midlertidigt blokeret fremtidige anmodninger. Vent venligst et øjeblik for du prøver igen."
},
"app.skins.rate-limit.title": {
"message": "Ro på!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Standard skins"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Du modtog disse skins for at donere til en Modrinth Pride indsamling imens der er Pride Month."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Gemte skins"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Log Ind"
},
"app.skins.sign-in.description": {
"message": "Venligst log ind til din Minecraft konto for at kunne funktionerne til at administrere dit skin i Modrinth appen."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Overrasket Modrinth Bot"
},
"app.skins.sign-in.title": {
"message": "Venligst log ind"
},
"app.skins.title": {
"message": "Skin vælger"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} er færdig med at download. Genindlæs for at opdatere nu, eller automatisk når du lukker Modrinth App."
@@ -392,14 +485,23 @@
"app.update.complete-toast.title": {
"message": "Version {version} var installeret med succes!"
},
"app.update.download-update": {
"message": "Download opdatering"
"app.world.server-modal.placeholder-address": {
"message": "eksemple.modrinth.gg"
},
"app.update.downloading-update": {
"message": "Downloader opdatering ({percent}%)"
"app.world.server-modal.select-an-option": {
"message": "Vælg en mulighed"
},
"app.update.reload-to-update": {
"message": "Genindlæs for at installere opdatering"
"app.world.world-item.incompatible-version": {
"message": "Ikke kompatibel version {version}"
},
"app.world.world-item.not-played-yet": {
"message": "Ikke spillet endnu"
},
"app.world.world-item.offline": {
"message": "Offline"
},
"app.world.world-item.players-online": {
"message": "{count} online"
},
"friends.action.add-friend": {
"message": "Tilføj en ven"
@@ -500,6 +602,12 @@
"instance.edit-world.title": {
"message": "Rediger verden"
},
"instance.files.adding-files": {
"message": "Tilføjer filer ({completed}/{total})"
},
"instance.files.save-as": {
"message": "Gem som..."
},
"instance.server-modal.address": {
"message": "Adresse"
},
@@ -614,6 +722,9 @@
"instance.settings.tabs.java": {
"message": "Java og hukommelse"
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Indskriv Java argumenter..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Omgivelses variabler"
},
@@ -629,6 +740,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Allokeret hukommelse"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/vej/til/java"
},
"instance.settings.tabs.window": {
"message": "Vindue"
},
@@ -701,9 +815,24 @@
"instance.worlds.world_in_use": {
"message": "Verden er i brug"
},
"minecraft-account.add-account": {
"message": "Tilføj konto"
},
"minecraft-account.label": {
"message": "Minecraft konto"
},
"minecraft-account.not-signed-in": {
"message": "Ikke logged ind"
},
"minecraft-account.remove-account": {
"message": "Fjern konto"
},
"minecraft-account.select-account": {
"message": "Vælg konto"
},
"minecraft-account.sign-in": {
"message": "Log ind på Minecraft"
},
"search.filter.locked.instance": {
"message": "Givet af instanceen"
},
@@ -727,5 +856,20 @@
},
"search.filter.locked.server-loader.title": {
"message": "Loader er givet af serveren"
},
"unknown-pack-warning-modal.body": {
"message": "En fil behandles kun, hvis den uploades til Modrinth, uanset filformat (herunder .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Vis ikke denne advarsel igen"
},
"unknown-pack-warning-modal.header": {
"message": "Bekræft installation"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Installer alligevel"
},
"unknown-pack-warning-modal.warning.title": {
"message": "Ukendt fil advarsel"
}
}
+19 -22
View File
@@ -86,6 +86,12 @@
"app.appearance-settings.select-option": {
"message": "Option wählen"
},
"app.appearance-settings.show-play-time.description": {
"message": "Zeigt an, wie viel Zeit du mit dem Spielen einer Instanz verbracht hast."
},
"app.appearance-settings.show-play-time.title": {
"message": "Spielzeit anzeigen"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Ermöglicht das Umschalten der Seitenleiste."
},
@@ -143,6 +149,9 @@
"app.browse.server.installing": {
"message": "Wird installiert"
},
"app.content-install.no-compatible-versions": {
"message": "Keine verfügbaren Versionen stimmen mit {compatibilityLabel} überein. Wähle eine Version aus, um sie trotzdem zu installieren. Abhängigkeiten werden nicht automatisch installiert."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -342,10 +351,10 @@
"message": "Bist du dir sicher, dass du diesen Skin löschen willst?"
},
"app.skins.dropped-file-error.text": {
"message": "Die abgelegte Datei konnte nicht gelesen werden."
"message": "Fehler beim Lesen der abgelegten Datei."
},
"app.skins.dropped-file-error.title": {
"message": "Fehler bei der Verarbeitung der Datei"
"message": "Fehler bei der Dateiverarbeitung"
},
"app.skins.edit-button": {
"message": "Skin bearbeiten"
@@ -381,7 +390,7 @@
"message": "Kein Umhang"
},
"app.skins.modal.none-cape-option": {
"message": "Keinen"
"message": "Keines"
},
"app.skins.modal.replace-texture-button": {
"message": "Textur ersetzen"
@@ -390,13 +399,13 @@
"message": "Skin speichern"
},
"app.skins.modal.saving-tooltip": {
"message": "Wird gespeichert..."
"message": "Speichern..."
},
"app.skins.modal.texture-section": {
"message": "Textur"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Lade zuerst ein Skin hoch!"
"message": "Lade zuerst einen Skin hoch!"
},
"app.skins.preview.edit-button": {
"message": "Skin bearbeiten"
@@ -405,10 +414,10 @@
"message": "Vorschau"
},
"app.skins.rate-limit.text": {
"message": "Du wechselst dein Skin zu oft. Die Server von Mojang haben weitere Anfragen vorübergehend blockiert. Bitte warte einen Moment, bevor du es erneut versuchst."
"message": "Du änderst deinen Skin zu oft. Die Server von Mojang haben weitere Anfragen vorübergehend blockiert. Bitte warte einen Moment, bevor du es erneut versuchst."
},
"app.skins.rate-limit.title": {
"message": "Langsamer!"
"message": "Nicht so schnell!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
@@ -429,7 +438,7 @@
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Du hast diese Skins erhalten, da du während des Pride-Monats für eine Modrinth-Pride-Spendenaktion gespendet hast."
"message": "Du hast diese Skins als Dankeschön für eine Spende bei der Modrinth Pride Spendenaktion während des Pride Month erhalten."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
@@ -453,20 +462,17 @@
"message": "Anmelden"
},
"app.skins.sign-in.description": {
"message": "Bitte melde dich bei deinem Minecraft-Konto an, um die Funktionen zur Skin-Verwaltung der Modrinth-App nutzen zu können."
"message": "Bitte melde dich in deinem Minecraft-Konto an, um die Skin-Verwaltungsfunktionen der Modrinth App zu nutzen."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Begeisterter Modrinth-Bot"
},
"app.skins.sign-in.title": {
"message": "Bitte melde dich an"
"message": "Bitte anmelden"
},
"app.skins.title": {
"message": "Skin-Auswahl"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} ist bereit zur Installation! Lade die App neu um jetzt zu aktualisieren, oder automatisch nach dem schliessen der Modrinth App."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} wurde heruntergeladen. Lade die App neu um jetzt zu aktualisieren, oder automatisch nach dem schliessen der Modrinth App."
},
@@ -497,15 +503,6 @@
"app.update.complete-toast.title": {
"message": "Version {version} wurde erfolgreich installiert!"
},
"app.update.download-update": {
"message": "Aktualisierung herunterladen"
},
"app.update.downloading-update": {
"message": "Lade Aktualisierung herunter ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Lade neu um Aktualisierung zu installieren"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
+10 -13
View File
@@ -48,7 +48,7 @@
"message": "Farbschema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Ändere die Kategorie, die beim Öffnen des Launchers angezeigt wird."
"message": "Ändere die Seite, die beim Öffnen des Launchers angezeigt wird."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Start"
@@ -86,6 +86,12 @@
"app.appearance-settings.select-option": {
"message": "Wähle eine Option"
},
"app.appearance-settings.show-play-time.description": {
"message": "Zeigt an, wie viel Zeit du damit verbracht hast, eine Instanz zu spielen."
},
"app.appearance-settings.show-play-time.title": {
"message": "Spielzeit anzeigen"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Ermöglicht das Umschalten der Seitenleiste."
},
@@ -143,6 +149,9 @@
"app.browse.server.installing": {
"message": "Wird installiert"
},
"app.content-install.no-compatible-versions": {
"message": "Keine verfügbaren Versionen stimmen mit {compatibilityLabel} überein. Wähle eine Version aus, um sie trotzdem zu installieren. Abhängigkeiten werden nicht automatisch installiert."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -464,9 +473,6 @@
"app.skins.title": {
"message": "Skin-Auswahl"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} ist bereit zur Installation! Neu laden, um jetzt zu aktualisieren, oder automatisch, wenn du die Modrinth App schließt."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} wurde heruntergeladen. Neu laden, um jetzt zu aktualisieren, oder automatisch aktualisieren, wenn du die Modrinth App schließt."
},
@@ -497,15 +503,6 @@
"app.update.complete-toast.title": {
"message": "Version {version} wurde erfolgreich installiert!"
},
"app.update.download-update": {
"message": "Update herunterladen"
},
"app.update.downloading-update": {
"message": "Update wird heruntergeladen ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Neu laden, um das Update zu installieren"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
+37 -13
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Downloading Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Downloading update"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Primary instance"
},
"app.action-bar.reload-to-update": {
"message": "Reload to update"
},
"app.action-bar.show-more-running-instances": {
"message": "Show more running instances"
},
"app.action-bar.stop-instance": {
"message": "Stop instance"
},
"app.action-bar.update": {
"message": "Update"
},
"app.action-bar.view-active-downloads": {
"message": "View active downloads"
},
@@ -149,6 +158,9 @@
"app.browse.server.installing": {
"message": "Installing"
},
"app.content-install.no-compatible-versions": {
"message": "No available versions match {compatibilityLabel}. Select a version to install anyway. Dependencies will not be installed automatically."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -479,9 +491,6 @@
"app.skins.title": {
"message": "Skin selector"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} is ready to install! Reload to update now, or automatically when you close Modrinth App."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} has finished downloading. Reload to update now, or automatically when you close Modrinth App."
},
@@ -501,7 +510,7 @@
"message": "Download complete"
},
"app.update-popup.reload": {
"message": "Reload"
"message": "Reload to update"
},
"app.update-popup.title": {
"message": "Update available"
@@ -512,15 +521,6 @@
"app.update.complete-toast.title": {
"message": "Version {version} was successfully installed!"
},
"app.update.download-update": {
"message": "Download update"
},
"app.update.downloading-update": {
"message": "Downloading update ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Reload to install update"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -710,6 +710,30 @@
"instance.settings.tabs.general.name": {
"message": "Name"
},
"instance.settings.tabs.general.update-channel": {
"message": "Update channel"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Release, beta, and alpha versions will be shown as available updates."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Release and beta versions will be shown as available updates."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Release"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Only release versions will be shown as available updates."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Select update channel"
},
"instance.settings.tabs.hooks": {
"message": "Launch hooks"
},
+18 -12
View File
@@ -86,6 +86,12 @@
"app.appearance-settings.select-option": {
"message": "Selecciona una opción"
},
"app.appearance-settings.show-play-time.description": {
"message": "Muestra cuánto tiempo has estado jugando en una instancia."
},
"app.appearance-settings.show-play-time.title": {
"message": "Mostrar tiempo de juego"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Permite mostrar u ocultar la barra lateral."
},
@@ -143,6 +149,9 @@
"app.browse.server.installing": {
"message": "Instalando"
},
"app.content-install.no-compatible-versions": {
"message": "No hay versiones disponibles que coincidan con {compatibilityLabel}. Selecciona una versión para instalarla de todas formas. Las dependencias no se instalarán automáticamente."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -422,6 +431,15 @@
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Orgullo Modrinth"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Has recibido estas skins por donar a la fundación Orgullo Modrinth durante el Mes del Orgullo."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Monturas del Caos"
},
@@ -455,9 +473,6 @@
"app.skins.title": {
"message": "Selector de skin"
},
"app.update-popup.body": {
"message": "¡Modrinth App v{version} está lista para instalarse! Actualiza ahora o automáticamente al cerrar la Modrinth App."
},
"app.update-popup.body.download-complete": {
"message": "La descarga de la Modrinth App v{version} ha finalizado. Actualiza ahora o automáticamente al cerrar la Modrinth App."
},
@@ -488,15 +503,6 @@
"app.update.complete-toast.title": {
"message": "¡La versión {version} se ha instalado correctamente!"
},
"app.update.download-update": {
"message": "Descargar actualización"
},
"app.update.downloading-update": {
"message": "Descargando actualización ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Recarga para instalar la actualización"
},
"app.world.server-modal.placeholder-address": {
"message": "ejemplo.modrinth.gg"
},
+158 -29
View File
@@ -9,7 +9,7 @@
"message": "Ocultar más instancias en ejecución"
},
"app.action-bar.make-primary-instance": {
"message": "Hacer instancia principal"
"message": "Establecer como instancia principal"
},
"app.action-bar.no-instances-running": {
"message": "Ninguna instancia en ejecución"
@@ -24,16 +24,16 @@
"message": "Mostrar más instancias en ejecución"
},
"app.action-bar.stop-instance": {
"message": "Finalizar instancia"
"message": "Detener instancia"
},
"app.action-bar.view-active-downloads": {
"message": "Mostrar descargas activas"
},
"app.action-bar.view-instance": {
"message": "Mostrar instancia"
"message": "Ver instancia"
},
"app.action-bar.view-logs": {
"message": "Mostrar registros"
"message": "Ver registros"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Habilita el renderizado avanzado, como los efectos de desenfoque, que pueden causar problemas de rendimiento sin renderizado acelerado por hardware."
@@ -42,43 +42,43 @@
"message": "Renderizado avanzado"
},
"app.appearance-settings.color-theme.description": {
"message": "Seleccione su tema de color preferido para Modrinth en este dispositivo."
"message": "Selecciona tu tema de color preferido para la Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema de color"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Cambia la página en la que el lanzador inicia."
"message": "Cambia la página que se abre al iniciar el lanzador."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Inicio"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Librería"
"message": "Biblioteca"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Página predeterminada"
"message": "Página de inicio predeterminada"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Desactiva la etiqueta arriba de tu personaje en la página de skins."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Ocultar etiqueta"
"message": "Ocultar etiqueta de nombre"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Incluye los mundos más recientes en la sección \"Vuelve a jugar tus mundos\" en la página de inicio."
"message": "Incluye los mundos recientes en la sección \"Volver a jugar\" de la página de inicio."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Vuelve a jugar tus mundos"
"message": "Volver a jugar en tus mundos"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimiza el lanzador cuando un proceso de Minecraft empieza."
"message": "Minimiza el lanzador cuando se inicia un proceso de Minecraft."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimiza el lanzador"
"message": "Minimizar lanzador"
},
"app.appearance-settings.native-decorations.description": {
"message": "Usa los sistemas de marco de Windows (se requiere resetear la aplicación)."
"message": "Usa el marco de ventana del sistema (requiere reiniciar la aplicación)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas"
@@ -86,6 +86,12 @@
"app.appearance-settings.select-option": {
"message": "Selecciona una opción"
},
"app.appearance-settings.show-play-time.description": {
"message": "Muestra cuánto tiempo has estado jugando en una instancia."
},
"app.appearance-settings.show-play-time.title": {
"message": "Mostrar tiempo de juego"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Activa la posibilidad de alternar la barra lateral."
},
@@ -96,7 +102,7 @@
"message": "Si intentas instalar un archivo de paquete de Modrinth (.mrpack) que no esté alojado en Modrinth, debes comprender los riesgos que esto conlleva antes de instalarlo."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Advertir antes de instalarme paquetes de mods desconocidos"
"message": "Adviérteme antes de instalarme modpacks desconocidos"
},
"app.auth-servers.unreachable.body": {
"message": "Los servidores de autenticación de Minecraft pueden no estar funcionando en este momento. Verifica tu conexión a internet e inténtalo de nuevo más tarde."
@@ -105,7 +111,7 @@
"message": "No se puede conectar con los servidores de autenticación"
},
"app.browse.add-servers-to-instance": {
"message": "Añadir servidor a instancia"
"message": "Añadiendo servidor a la instancia"
},
"app.browse.add-to-an-instance": {
"message": "Añadir a una instancia"
@@ -135,7 +141,7 @@
"message": "Ocultar servidores ya añadidos"
},
"app.browse.project-type.modpacks": {
"message": "Paquetes de Mods"
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Añadir contenido puede comprometer la compatibilidad a la hora de unirse a un servidor. Cualquier contenido añadido se perderá cuando actualices la instancia del servidor."
@@ -143,11 +149,14 @@
"app.browse.server.installing": {
"message": "Instalando"
},
"app.content-install.no-compatible-versions": {
"message": "No hay versiones disponibles que coincidan con {compatibilityLabel}. Selecciona una versión para instalarla de todas formas. Las dependencias no se instalarán automáticamente."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Instalando el modpack..."
"message": "Instalando modpack..."
},
"app.export-modal.description-placeholder": {
"message": "Escribe la descripción del modpack..."
@@ -341,8 +350,137 @@
"app.skins.apply-button": {
"message": "Aplicar"
},
"app.update-popup.body": {
"message": "¡La versión v{version} de Modrinth está lista para instalarse! Actualiza ahora o automáticamente al cerrar la aplicación."
"app.skins.delete-button": {
"message": "Borrar skin"
},
"app.skins.delete-modal.description": {
"message": "Esto borrará permanentemente la skin seleccionada. Esta acción no se puede deshacer."
},
"app.skins.delete-modal.title": {
"message": "¿Estás seguro/a de que quieres eliminar está skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Error al leer el archivo arrastrado."
},
"app.skins.dropped-file-error.title": {
"message": "Error al procesar el archivo"
},
"app.skins.edit-button": {
"message": "Editar skin"
},
"app.skins.modal.add-skin-button": {
"message": "Añadir skin"
},
"app.skins.modal.add-title": {
"message": "Añadiendo una skin"
},
"app.skins.modal.arm-style-section": {
"message": "Estilo de brazo"
},
"app.skins.modal.arm-style-slim": {
"message": "Delgado"
},
"app.skins.modal.arm-style-wide": {
"message": "Ancho"
},
"app.skins.modal.cape-fallback-name": {
"message": "Capa"
},
"app.skins.modal.cape-section": {
"message": "Capa"
},
"app.skins.modal.edit-title": {
"message": "Editando skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "¡Modifica la skin primero!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Sin capa"
},
"app.skins.modal.none-cape-option": {
"message": "Nada"
},
"app.skins.modal.replace-texture-button": {
"message": "Reemplazar textura"
},
"app.skins.modal.save-skin-button": {
"message": "Guardar skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Guardando..."
},
"app.skins.modal.texture-section": {
"message": "Textura"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "¡Primero sube una skin!"
},
"app.skins.preview.edit-button": {
"message": "Editar skin"
},
"app.skins.previewing-badge": {
"message": "Previsualizando"
},
"app.skins.rate-limit.text": {
"message": "Estuviste cambiando tu skin demasiado rápido. Los servidores de Mojang han bloqueado temporalmente tus futuras solicitaciones. Por favor, espere un momento antes de intentar de nuevo."
},
"app.skins.rate-limit.title": {
"message": "¡Cálmate!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders y Biomas"
},
"app.skins.section.chase-the-skies": {
"message": "Persigue los cielos"
},
"app.skins.section.default-skins": {
"message": "Skins por defecto"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Orgullo Modrinth"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Has recibido estas skins por donar a la fundación Orgullo Modrinth durante el Mes del Orgullo."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Monturas del Caos"
},
"app.skins.section.saved-skins": {
"message": "Skins guardadas"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "La Edad de Cobre"
},
"app.skins.section.the-garden-awakens": {
"message": "El Jardín Despierta"
},
"app.skins.section.tiny-takeover": {
"message": "Pequeña Invasión"
},
"app.skins.sign-in.button": {
"message": "Iniciar Sesión"
},
"app.skins.sign-in.description": {
"message": "Por favor, inicia sesión con tu cuenta de Minecraft para usar las funciones del administrador de Skins de la app de Modrinth."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Modrinth Bot emocionado"
},
"app.skins.sign-in.title": {
"message": "Por favor, inicia sesión"
},
"app.skins.title": {
"message": "Selector de Skin"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} se ha descargado. Actualiza la página ahora o espera a que se actualice automáticamente al cerrar Modrinth App."
@@ -374,15 +512,6 @@
"app.update.complete-toast.title": {
"message": "¡La versión {version} se ha instalado correctamente!"
},
"app.update.download-update": {
"message": "Descarga actualización"
},
"app.update.downloading-update": {
"message": "Descargando actualización ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Recarga para instalar la actualización"
},
"app.world.server-modal.placeholder-address": {
"message": "ejemplo.modrinth.gg"
},
@@ -332,9 +332,6 @@
"app.settings.tabs.resource-management": {
"message": "Resurssien hallinta"
},
"app.update-popup.body": {
"message": "Modrinth App versio {version} on valmis asennettavaksi. Voit käynnistää sovelluksen uudelleen päivittääksesi heti tai antaa päivityksen asentua automaattisesti, kun suljet Modrinth Appin."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App versio {version} on ladattu. Voit käynnistää sovelluksen uudelleen päivittääksesi heti tai antaa päivityksen asentua automaattisesti, kun suljet Modrinth Appin."
},
@@ -365,15 +362,6 @@
"app.update.complete-toast.title": {
"message": "Versio {version} asennettiin onnistuneesti!"
},
"app.update.download-update": {
"message": "Lataa päivitys"
},
"app.update.downloading-update": {
"message": "Ladataan päivitystä ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Lataa uudelleen asentaaksesi päivityksen"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -359,9 +359,6 @@
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.update-popup.body": {
"message": "Ang Modrinth App v{version} ay handa nang ma-install. Mag-reload upang ma-update ngayon, o awtomatiko sa pagsara ng Modrinth App."
},
"app.update-popup.body.download-complete": {
"message": "Tapos nang ma-download ang Modrinth App v{version}. Mag-reload upang ma-update ngayon, o awtomatiko sa pagsara ng Modrinth App."
},
@@ -392,15 +389,6 @@
"app.update.complete-toast.title": {
"message": "Tagumpay na na-install ang bersiyong {version}!"
},
"app.update.download-update": {
"message": "I-download ang update"
},
"app.update.downloading-update": {
"message": "Nagdadownload ng update ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Handang ma-install ang update"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
+21 -24
View File
@@ -45,13 +45,13 @@
"message": "Sélectionnez votre couleur préférée pour le thème de Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Thème couleur"
"message": "Couleur du thème"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Modifier la page sur laquelle le lancheur s'ouvre."
"message": "Modifier la page sur laquelle le lanceur s'ouvre."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Acceuil"
"message": "Accueil"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Bibliothèque"
@@ -72,10 +72,10 @@
"message": "Mondes sur partie rapide"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimiser le launcher quand Minecraft démarre."
"message": "Minimiser le lanceur quand Minecraft démarre."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimiser le launcher"
"message": "Minimiser le lanceur"
},
"app.appearance-settings.native-decorations.description": {
"message": "Utiliser le cadre fenêtre du système (redémarrage de l'application requis)."
@@ -86,8 +86,14 @@
"app.appearance-settings.select-option": {
"message": "Sélectionnez une option"
},
"app.appearance-settings.show-play-time.description": {
"message": "Affiche combien de temps vous avez passé sur une instance."
},
"app.appearance-settings.show-play-time.title": {
"message": "Afficher le temps de jeu"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Permet d'ouvrir de de fermer la barre latérale."
"message": "Permet d'ouvrir de fermer la barre latérale."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Tiroir latéral"
@@ -143,6 +149,9 @@
"app.browse.server.installing": {
"message": "Installation en cours"
},
"app.content-install.no-compatible-versions": {
"message": "Aucune version disponible ne correspond à {compatibilityLabel}. Sélectionnez une version pour l'installer malgré tout. Les dépendances ne seront pas installées automatiquement."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -333,10 +342,10 @@
"message": "Appliquer"
},
"app.skins.delete-button": {
"message": "Supprimer la skin"
"message": "Supprimer le skin"
},
"app.skins.delete-modal.description": {
"message": "Cela va supprimer de manière permanente cette skin. Cette action est irréversible."
"message": "Cela va supprimer de manière permanente ce skin. Cette action est irréversible."
},
"app.skins.delete-modal.title": {
"message": "Êtes-vous sûr de vouloir supprimer cette skin ?"
@@ -348,7 +357,7 @@
"message": "Erreur lors du traitement du fichier"
},
"app.skins.edit-button": {
"message": "Modifier la skin"
"message": "Modifier le skin"
},
"app.skins.modal.add-skin-button": {
"message": "Ajouter une skin"
@@ -372,7 +381,7 @@
"message": "Cape"
},
"app.skins.modal.edit-title": {
"message": "Modification de la skin"
"message": "Modification du skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Faites d'abord une modification à la skin !"
@@ -387,7 +396,7 @@
"message": "Remplacer la texture"
},
"app.skins.modal.save-skin-button": {
"message": "Sauvegarder la skin"
"message": "Sauvegarder le skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Sauvegarde..."
@@ -399,7 +408,7 @@
"message": "Importez d'abord une skin !"
},
"app.skins.preview.edit-button": {
"message": "Modifier la skin"
"message": "Modifier le skin"
},
"app.skins.previewing-badge": {
"message": "Aperçu"
@@ -464,9 +473,6 @@
"app.skins.title": {
"message": "Sélecteur de skin"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} est prête à être installée ! Relancez l'application pour faire la mise à jour maintenant, ou automatiquement à la fermeture de Modrinth App."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} a finie d'être téléchargée. Rechargez pour mettre à jour maintenant, ou automatiquement quand vous fermez Modrinth App."
},
@@ -497,15 +503,6 @@
"app.update.complete-toast.title": {
"message": "La version {version} a été téléchargée avec succès !"
},
"app.update.download-update": {
"message": "Télécharger la mise à jour"
},
"app.update.downloading-update": {
"message": "Téléchargement de la mise à jour ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Relancez l'application pour installer la mise à jour"
},
"app.world.server-modal.placeholder-address": {
"message": "exemple.modrinth.gg"
},
@@ -203,9 +203,6 @@
"app.settings.tabs.resource-management": {
"message": "ניהול משאבים"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} מוכנה להורדה!\nיש לרענן כדי לעדכן עכשיו, או באופן אוטומטי בעת סגירת האפליקציה."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} סיימה את תהליך ההורדה. יש לרענן כדי לעדכן עכשיו, או באופן אוטומטי בעת סגירת האפליקציה."
},
@@ -236,15 +233,6 @@
"app.update.complete-toast.title": {
"message": "גרסה {version} הותקנה בהצלחה!"
},
"app.update.download-update": {
"message": "הורדת עדכון"
},
"app.update.downloading-update": {
"message": "מוריד עדכון ({percent}%)"
},
"app.update.reload-to-update": {
"message": "צריך לרענן כדי להתקין את העדכון"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
+24 -18
View File
@@ -86,6 +86,12 @@
"app.appearance-settings.select-option": {
"message": "Válassz egy lehetőséget"
},
"app.appearance-settings.show-play-time.description": {
"message": "Megmutatja, mennyi időt töltöttél játszva egy profillal."
},
"app.appearance-settings.show-play-time.title": {
"message": "Játékidő megjelenítése"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Lehetővé teszi az oldalsáv be- és kikapcsolását."
},
@@ -105,7 +111,7 @@
"message": "Nem lehet elérni a hitelesítési kiszolgálókat"
},
"app.browse.add-servers-to-instance": {
"message": "Szerver hozzáadása az példányhoz"
"message": "Szerver hozzáadása a példányhoz"
},
"app.browse.add-to-an-instance": {
"message": "Hozzáadás egy példányhoz"
@@ -143,6 +149,9 @@
"app.browse.server.installing": {
"message": "Letöltés..."
},
"app.content-install.no-compatible-versions": {
"message": "Nincs a(z) {compatibilityLabel} verziónak megfelelő elérhető verzió. Válassz ki egy verziót, ha mégis telepíteni szeretnéd. A függőségek nem települnek automatikusan."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -428,6 +437,15 @@
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Ezeket a kinézeteket azért kaptad, mert a Pride-hónap során adományoztál a Modrinth Pride jótékonysági gyűjtéséhez."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
@@ -461,9 +479,6 @@
"app.skins.title": {
"message": "Kinézetváltó"
},
"app.update-popup.body": {
"message": "A Modrinth App v{version} telepítésre kész! Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás a bezárásakor automatikusan frissül."
},
"app.update-popup.body.download-complete": {
"message": "A Modrinth App v{version} letöltése befejeződött. Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás a bezárásakor automatikusan frissül."
},
@@ -494,15 +509,6 @@
"app.update.complete-toast.title": {
"message": "Verzió {version} sikeresen telepítve!"
},
"app.update.download-update": {
"message": "Frissítés letöltése"
},
"app.update.downloading-update": {
"message": "Frissítés letöltése ({percent}%)"
},
"app.update.reload-to-update": {
"message": "A telepítéshez újraindítás szükséges"
},
"app.world.server-modal.placeholder-address": {
"message": "pelda.modrinth.gg"
},
@@ -816,7 +822,7 @@
"message": "Ne mutasd a kezdőlapon"
},
"instance.worlds.game_already_open": {
"message": "A profil már el van indítva"
"message": "A példány már el van indítva"
},
"instance.worlds.hardcore": {
"message": "Hardcore mód"
@@ -840,7 +846,7 @@
"message": "Játék a profillal"
},
"instance.worlds.view_instance": {
"message": "Profil megtekintése"
"message": "Példány megtekintése"
},
"instance.worlds.world_in_use": {
"message": "A világ használatban van"
@@ -864,13 +870,13 @@
"message": "Bejelentkezés a Minecraftba"
},
"search.filter.locked.instance": {
"message": "A profil által van megadva"
"message": "Pédány által megadva"
},
"search.filter.locked.instance-game-version.title": {
"message": "A játékverzió a profil által van megadva"
"message": "A játékverziót az adott példány biztosítja"
},
"search.filter.locked.instance-loader.title": {
"message": "A betöltő a profil által van megadva"
"message": "A betöltőt az adott példány biztosítja"
},
"search.filter.locked.instance.sync": {
"message": "Profil szinkronizálása"
@@ -470,9 +470,6 @@
"app.skins.title": {
"message": "Pemilih rupa"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} siap dipasang! Muat ulang untuk memperbarui sekarang, atau secara otomatis saat Anda menutup Modrinth App."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} telah selesai mengunduh. Muat ulang untuk memperbarui sekarang, atau secara otomatis saat Anda menutup Modrinth App."
},
@@ -503,15 +500,6 @@
"app.update.complete-toast.title": {
"message": "Versi {version} berhasil dipasang!"
},
"app.update.download-update": {
"message": "Unduh pembaruan"
},
"app.update.downloading-update": {
"message": "Mengunduh pembaruan ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Muat ulang untuk memasang pembaruan"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
+12 -15
View File
@@ -86,14 +86,20 @@
"app.appearance-settings.select-option": {
"message": "Seleziona"
},
"app.appearance-settings.show-play-time.description": {
"message": "Mostra quanto tempo hai speso giocando a un'istanza."
},
"app.appearance-settings.show-play-time.title": {
"message": "Mostra tempo di gioco"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Scegli se mostrare o nascondere la barra laterale."
"message": "Mostra o nascondi la barra laterale."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Abilita la barra laterale"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Visti i rischi, apparirà un avviso prima di installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth."
"message": "Mostrerà un avviso prima di installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth, a causa dei possibili rischi."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Avvisami se installo pacchetti sconosciuti"
@@ -138,11 +144,14 @@
"message": "Pacchetti di mod"
},
"app.browse.server-instance-content-warning": {
"message": "Aggiungere dei contenuti potrebbe portare problemi entrando nel server. Qualsiasi contenuto aggiunto sarà anche perso aggiornando i contenuti dell'istanza del server."
"message": "Aggiungere dei contenuti potrebbe causare problemi entrando nel server. Inoltre, qualsiasi contenuto aggiunto sarà perso a ogni aggiornamento dei contenuti dell'istanza."
},
"app.browse.server.installing": {
"message": "Installazione"
},
"app.content-install.no-compatible-versions": {
"message": "Nessuna versione disponibile corrisponde a {compatibilityLabel}. Seleziona una versione da installare comunque. Le dipendenze non verranno scaricate automaticamente."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -461,9 +470,6 @@
"app.skins.title": {
"message": "Seleziona una skin"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} è pronta per essere installata! Ricarica per aggiornare ora, o avverrà in automatico alla chiusura dell'app."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} è stata scaricata. Ricarica per aggiornare ora, o avverrà in automatico alla chiusura dell'app."
},
@@ -494,15 +500,6 @@
"app.update.complete-toast.title": {
"message": "La versione {version} è stata installata con successo!"
},
"app.update.download-update": {
"message": "Scarica aggiornamento"
},
"app.update.downloading-update": {
"message": "Scaricando l'aggiornamento ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Ricarica per installare l'aggiornamento"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
+11 -11
View File
@@ -302,8 +302,14 @@
"app.settings.tabs.resource-management": {
"message": "リソース管理"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} のインストール準備が整いました!今すぐ更新するには再読み込みするか、Modrinth Appを閉じる際に自動更新されます。"
"app.skins.modal.replace-texture-button": {
"message": "置きかえる"
},
"app.skins.modal.texture-section": {
"message": "テクスチャ"
},
"app.skins.preview.edit-button": {
"message": "編集"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} のダウンロードが完了しました。今すぐ更新するには再読み込みするか、Modrinth Appを閉じる際に自動更新されます。"
@@ -335,15 +341,6 @@
"app.update.complete-toast.title": {
"message": "バージョン {version} が正常にインストールされました!"
},
"app.update.download-update": {
"message": "アップデートをダウンロード"
},
"app.update.downloading-update": {
"message": "アップデートをダウンロード中 ({percent}%)"
},
"app.update.reload-to-update": {
"message": "再起動して今すぐ更新"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -593,6 +590,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "メモリ割り当て"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "ウィンドウ"
},
+21 -12
View File
@@ -86,6 +86,12 @@
"app.appearance-settings.select-option": {
"message": "옵션 선택"
},
"app.appearance-settings.show-play-time.description": {
"message": "인스턴스의 플레이 시간을 표시합니다."
},
"app.appearance-settings.show-play-time.title": {
"message": "플레이 시간 표시"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "사이드바를 접거나 펼칠 수 있는 기능을 활성화합니다."
},
@@ -143,6 +149,9 @@
"app.browse.server.installing": {
"message": "설치 중"
},
"app.content-install.no-compatible-versions": {
"message": "{compatibilityLabel} 버전과 일치하는 버전이 없습니다. 설치할 버전을 선택하세요. 종속성은 자동으로 설치되지 않습니다."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -431,6 +440,15 @@
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth 프라이드"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "프라이드 월간 동안 Modrinth 프라이드 모금 행사에 기부하여 이 스킨들을 받았습니다."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
@@ -455,15 +473,15 @@
"app.skins.sign-in.description": {
"message": "Modrinth 앱의 스킨 관리 기능을 사용하려면 Minecraft 계정에 로그인하세요."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Excited Modrinth Bot"
},
"app.skins.sign-in.title": {
"message": "로그인하세요"
},
"app.skins.title": {
"message": "스킨 선택"
},
"app.update-popup.body": {
"message": "Modrinth App v{version}을 설치할 준비가 완료되었습니다! 새로고침하거나 Modrinth App을 종료하면 자동으로 업데이트됩니다."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} 다운로드가 완료되었습니다. 새로고침하거나 Modrinth App을 종료하면 자동으로 업데이트됩니다."
},
@@ -494,15 +512,6 @@
"app.update.complete-toast.title": {
"message": "{version} 버전이 성공적으로 설치되었습니다!"
},
"app.update.download-update": {
"message": "업데이트 다운로드"
},
"app.update.downloading-update": {
"message": "업데이트 다운로드 중 ({percent}%)"
},
"app.update.reload-to-update": {
"message": "새로고침하여 업데이트 설치"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
+110 -11
View File
@@ -143,6 +143,9 @@
"app.browse.server.installing": {
"message": "Sedang memasang"
},
"app.content-install.no-compatible-versions": {
"message": "Tiada versi tersedia yang sepadan dengan {compatibilityLabel}. Pilih versi untuk dipasang juga. Kebergantungan tidak akan dipasang secara automatik."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -329,8 +332,110 @@
"app.settings.tabs.resource-management": {
"message": "Pengurusan sumber"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} sudah bersedia untuk dipasang! Muat semula untuk kemas kini sekarang, atau kemas kini secara automatik apabila anda menutup Modrinth App."
"app.skins.add-button": {
"message": "Tambah kekulit"
},
"app.skins.apply-button": {
"message": "Terapkan"
},
"app.skins.delete-button": {
"message": "Padam kekulit"
},
"app.skins.delete-modal.description": {
"message": "Ini akan memadamkan kekulit yang dipilih selama-lamanya. Tindakan ini tidak boleh dibuat asal."
},
"app.skins.edit-button": {
"message": "Sunting kekulit"
},
"app.skins.modal.add-skin-button": {
"message": "Tambah kekulit"
},
"app.skins.modal.arm-style-section": {
"message": "Gaya lengan"
},
"app.skins.modal.arm-style-slim": {
"message": "Kurus"
},
"app.skins.modal.arm-style-wide": {
"message": "Lebar"
},
"app.skins.modal.cape-fallback-name": {
"message": "Mantel"
},
"app.skins.modal.cape-section": {
"message": "Mantel"
},
"app.skins.modal.edit-title": {
"message": "Menyunting kekulit"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Tiada mantel"
},
"app.skins.modal.none-cape-option": {
"message": "Tiada"
},
"app.skins.modal.save-skin-button": {
"message": "Simpan kekulit"
},
"app.skins.modal.saving-tooltip": {
"message": "Sedang menyimpan..."
},
"app.skins.modal.texture-section": {
"message": "Tekstur"
},
"app.skins.preview.edit-button": {
"message": "Sunting kekulit"
},
"app.skins.previewing-badge": {
"message": "Sedang pralihat"
},
"app.skins.rate-limit.text": {
"message": "Anda terlalu kerap menukar kekulit anda. Pelayan Mojang telah menyekat permintaan selanjutnya buat sementara waktu. Sila tunggu sebentar sebelum mencuba lagi."
},
"app.skins.rate-limit.title": {
"message": "Perlahanlah sikit!"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Anda menerima kekulit ini kerana menderma kepada acara pengumpulan dana Modrinth Pride semasa Pride Month."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Kekulit yang disimpan"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Daftar Masuk"
},
"app.skins.sign-in.description": {
"message": "Sila daftar masuk ke akaun Minecraft anda untuk menggunakan ciri pengurusan kekulit milik Modrinth App."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Bot Modrinth yang Teruja"
},
"app.skins.sign-in.title": {
"message": "Sila daftar masuk"
},
"app.skins.title": {
"message": "Pemilih kekulit"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} sudah selesai dimuat turun! Muat semula untuk kemas kini sekarang, atau kemas kini secara automatik apabila anda menutup Modrinth App."
@@ -362,15 +467,6 @@
"app.update.complete-toast.title": {
"message": "Versi {version} telah berjaya dipasang!"
},
"app.update.download-update": {
"message": "Muat turun kemas kini"
},
"app.update.downloading-update": {
"message": "Sedang memuat turun kemas kini ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Muat semula untuk memasang kemas kini"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -617,6 +713,9 @@
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Peruntukan ingatan tersuai"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Masukkan pembolehubah persekitaran..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Masukkan argumen Java..."
},
@@ -323,9 +323,6 @@
"app.settings.tabs.resource-management": {
"message": "Bronnenbeheer"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} is klaar om geïnstalleerd te worden! Herlaad om nu te updaten, of automatisch wanneer je de Modrinth App afsluit."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} is klaar met downloaden. Herlaad om nu te updaten, of automatisch wanneer je de Modrinth App afsluit."
},
@@ -356,15 +353,6 @@
"app.update.complete-toast.title": {
"message": "Versie {version} is succesvol geïnstalleerd!"
},
"app.update.download-update": {
"message": "Download update"
},
"app.update.downloading-update": {
"message": "Update downloaden ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Herlaad om de update te installeren"
},
"app.world.server-modal.placeholder-address": {
"message": "voorbeeld.modrinth.gg"
},
@@ -59,15 +59,6 @@
"app.update.complete-toast.title": {
"message": "Versjon {version} ble installert!"
},
"app.update.download-update": {
"message": "Last ned oppdatering"
},
"app.update.downloading-update": {
"message": "Laster ned oppdatering ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Last inn på nytt for å installere oppdateringen"
},
"friends.action.add-friend": {
"message": "Legg til en venn"
},
+96 -21
View File
@@ -86,6 +86,12 @@
"app.appearance-settings.select-option": {
"message": "Wybierz opcję"
},
"app.appearance-settings.show-play-time.description": {
"message": "Pokazuje czas gry spędzony w danej instancji."
},
"app.appearance-settings.show-play-time.title": {
"message": "Pokaż czas gry"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Umożliwia przełączenie paska bocznego."
},
@@ -143,6 +149,9 @@
"app.browse.server.installing": {
"message": "Instalowanie"
},
"app.content-install.no-compatible-versions": {
"message": "Nie znaleziono żadnych wersji pasujących do {compatibilityLabel}. Wybierz wersję, by zainstalować ją mimo tego. Zależności nie zostaną zainstalowane automatycznie."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -327,13 +336,28 @@
"message": "Zarządzanie zasobami"
},
"app.skins.add-button": {
"message": "Dodaj skina"
"message": "Dodaj skórkę"
},
"app.skins.add-button.drag-and-drop": {
"message": "Przeciągnij i upuść"
},
"app.skins.apply-button": {
"message": "Zastosuj"
},
"app.skins.delete-button": {
"message": "Usuń skina"
"message": "Usuń skórkę"
},
"app.skins.delete-modal.description": {
"message": "To trwale usunie wybraną skórkę. Tej akcji nie można odwrócić."
},
"app.skins.delete-modal.title": {
"message": "Czy na pewno chcesz usunąć tą skórkę?"
},
"app.skins.dropped-file-error.text": {
"message": "Nie udało się odczytać wybranego pliku."
},
"app.skins.dropped-file-error.title": {
"message": "Błąd podczas przetwarzania pliku"
},
"app.skins.edit-button": {
"message": "Edytuj skina"
@@ -344,20 +368,35 @@
"app.skins.modal.add-title": {
"message": "Dodawanie skina"
},
"app.skins.modal.arm-style-section": {
"message": "Styl ramion"
},
"app.skins.modal.arm-style-slim": {
"message": "Smukły"
},
"app.skins.modal.arm-style-wide": {
"message": "Szeroki"
},
"app.skins.modal.cape-fallback-name": {
"message": "Peleryna"
},
"app.skins.modal.cape-section": {
"message": "Pelerynki"
"message": "Peleryny"
},
"app.skins.modal.edit-title": {
"message": "Edytuj skina"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Wpierw zmień jakoś skórkę!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Brak peleryny"
},
"app.skins.modal.none-cape-option": {
"message": "Nic"
"message": "Brak"
},
"app.skins.modal.replace-texture-button": {
"message": "Zamień teksturę"
},
"app.skins.modal.save-skin-button": {
"message": "Zapisz skina"
@@ -368,29 +407,74 @@
"app.skins.modal.texture-section": {
"message": "Tekstura"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Wpierw prześlij skórkę!"
},
"app.skins.preview.edit-button": {
"message": "Edytuj skina"
"message": "Edytuj skórkę"
},
"app.skins.previewing-badge": {
"message": "Podgląd"
},
"app.skins.rate-limit.text": {
"message": "Zbyt często zmieniasz swoją skórkę. Serwery Mojang tymczasowo zablokowały dalsze żądania. Odczekaj chwilę i spróbuj ponownie."
},
"app.skins.rate-limit.title": {
"message": "Zwolnij!"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Domyślne skiny"
"message": "Domyślne skórki"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Duma Modrinth"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Zdobyłeś te skórki poprzez dotację na zbiórkę Dumy Modrinth w okresie Miesiąca Dumy."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Zapisane skórki"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Zaloguj Się"
"message": "Zaloguj się"
},
"app.skins.sign-in.description": {
"message": "Zaloguj się na konto Minecraft, by móc zarządzać skórkami poprzez Modrinth App."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Podekscytowany Modrinth Bot"
},
"app.skins.sign-in.title": {
"message": "Proszę zaloguj się"
"message": "Zaloguj się"
},
"app.skins.title": {
"message": "Wybierz skina"
},
"app.update-popup.body": {
"message": "Wersja Modrinth App v{version} jest gotowa do zainstalowania! Załaduj ponownie, żeby zaktualizować teraz, albo automatycznie, gdy zamkniesz Modrinth App."
"message": "Wybierz skórkę"
},
"app.update-popup.body.download-complete": {
"message": "Wersja Modrinth App v{version} została pobrana. Załaduj ponownie, żeby zaktualizować teraz, albo automatycznie, gdy zamkniesz Modrinth App."
@@ -422,15 +506,6 @@
"app.update.complete-toast.title": {
"message": "Wersja {version} została pomyślnie zainstalowana!"
},
"app.update.download-update": {
"message": "Pobierz aktualizację"
},
"app.update.downloading-update": {
"message": "Pobieranie aktualizacji ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Załaduj ponownie, aby zainstalować aktualizację"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
+76 -76
View File
@@ -1,6 +1,6 @@
{
"app.action-bar.downloading-java": {
"message": "Baixando java {version}"
"message": "Baixando Java {version}"
},
"app.action-bar.downloads": {
"message": "Downloads"
@@ -9,19 +9,19 @@
"message": "Ocultar mais instâncias em execução"
},
"app.action-bar.make-primary-instance": {
"message": "Criar instância primária"
"message": "Tornar instância principal"
},
"app.action-bar.no-instances-running": {
"message": "Nenhuma instância em execução"
},
"app.action-bar.offline": {
"message": "Offline"
"message": "Inativo"
},
"app.action-bar.primary-instance": {
"message": "Instância primária"
"message": "Instância principal"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar mais instâncias em execução"
"message": "Exibir mais instâncias em execução"
},
"app.action-bar.stop-instance": {
"message": "Parar instância"
@@ -33,10 +33,10 @@
"message": "Ver instância"
},
"app.action-bar.view-logs": {
"message": "Ver logs"
"message": "Ver registros"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Ativa a renderização avançada, como efeitos de desfoque que podem causar problemas de desempenho sem a renderização acelerada de hardware."
"message": "Ativa a renderização avançada, como desfoque que talvez afete o desempenho sem renderização acelerada do hardware."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Renderização avançada"
@@ -45,10 +45,10 @@
"message": "Selecione seu tema de cores preferido para o Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema de Cores"
"message": "Tema de cores"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Alterar a página em que o launcher é aberto."
"message": "Altera a página que o launcher é aberto."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Início"
@@ -60,25 +60,25 @@
"message": "Página inicial padrão"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Desativa o nome acima do seu jogador na página de skins."
"message": "Desativa o nametag acima do seu jogador na página de Skins."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Ocultar nome"
"message": "Ocultar nametag"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Inclui mundos recentes na seção \"Voltar à ação\" na página inicial."
"message": "Inclui mundos recentes na seção \"Retomar\" na página inicial."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Volte aos mundos"
"message": "Retomar aos mundos"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimize o launcher quando um processo do Minecraft for iniciado."
"message": "Minimize o launcher assim que algum processo do Minecraft iniciar."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimizar launcher"
},
"app.appearance-settings.native-decorations.description": {
"message": "Usar molduras de janela do sistema (requer reiniciar o aplicativo)."
"message": "Usar molduras de janela do sistema (exige reinicialização do aplicativo)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Decorações nativas"
@@ -86,14 +86,20 @@
"app.appearance-settings.select-option": {
"message": "Selecione uma opção"
},
"app.appearance-settings.show-play-time.description": {
"message": "Exibe a quantidade de tempo jogado de uma instância."
},
"app.appearance-settings.show-play-time.title": {
"message": "Exibir tempo jogado"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Ativa a capacidade de alternar a visibilidade da barra lateral."
"message": "Permite alternar a barra lateral."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Alternar barra lateral"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Se você tentar instalar um arquivo Modrinth Pack (.mrpack) que não esteja hospedado no Modrinth, garantiremos que você entenda os riscos antes de instalá-lo."
"message": "Se você tentar instalar um arquivo Modrinth Pack (.mrpack) não hospedado no Modrinth, garantiremos que você entenda os riscos antes de instalá-lo."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Avise-me antes de instalar pacotes de mods desconhecidos"
@@ -123,13 +129,13 @@
"message": "Já adicionado"
},
"app.browse.back-to-instance": {
"message": "Voltar a instância"
"message": "Voltar à instância"
},
"app.browse.discover-content": {
"message": "Descubra conteúdo"
"message": "Descobrir conteúdo"
},
"app.browse.discover-servers": {
"message": "Descubra servidores"
"message": "Descobrir servidores"
},
"app.browse.hide-added-servers": {
"message": "Ocultar servidores já adicionados"
@@ -138,11 +144,14 @@
"message": "Pacotes de mods"
},
"app.browse.server-instance-content-warning": {
"message": "Adicionar conteúdo pode quebrar a compatibilidade ao entrar no servidor. Qualquer conteúdo adicionado também será perdido ao atualizar o conteúdo da instância do servidor."
"message": "Adicionar conteúdo talvez quebre a compatibilidade ao entrar no servidor. Qualquer conteúdo adicionado será perdido ao atualizar o conteúdo da instância do servidor."
},
"app.browse.server.installing": {
"message": "Instalando"
},
"app.content-install.no-compatible-versions": {
"message": "Nenhuma versão disponível corresponde a {compatibilityLabel}. Selecione uma versão para instalar mesmo assim. As dependências não serão instaladas automaticamente."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -168,7 +177,7 @@
"message": "Nome do pacote de mods"
},
"app.export-modal.select-files-label": {
"message": "Configure quais arquivos serão incluídos nessa exportação"
"message": "Configure quais arquivos serão incluídos na exportação"
},
"app.export-modal.version-number-label": {
"message": "Número da versão"
@@ -177,7 +186,7 @@
"message": "1.0.0"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Todos os dados da sua instância serão permanentemente excluídos, incluindo seus mundos, configurações e todo o conteúdo instalado."
"message": "Todos os dados para sua instância serão excluídos permanentemente, incluindo seus mundos, configs e todo o conteúdo instalado."
},
"app.instance.confirm-delete.admonition-header": {
"message": "Esta ação não pode ser desfeita"
@@ -189,7 +198,7 @@
"message": "Excluir instância"
},
"app.instance.modpack-already-installed.body": {
"message": "Este pacote de mods já está instalado na instância <bold>{instanceName}</bold>. Tem certeza de que deseja duplicá-lo?"
"message": "Este pacote de mods já foi instalado na instância <bold>{instanceName}</bold>. Deseja mesmo duplicá-lo?"
},
"app.instance.modpack-already-installed.create": {
"message": "Criar"
@@ -210,10 +219,10 @@
"message": "{count} projetos foram adicionados"
},
"app.instance.mods.share-text": {
"message": "Confira os projetos que estou usando no meu pacote de mods!"
"message": "Dê uma olhada nos projetos que estou usando no meu pacote de mods!"
},
"app.instance.mods.share-title": {
"message": "Compartilhando conteúdo do pacote de mods\n\n\n \t\t\t\t\t\t\n \t\t\t\t\t\t\t\n \t\t\t\t\t\t\n \t\t\t\t\t\t"
"message": "Compartilhando conteúdo do pacote de mods"
},
"app.instance.mods.successfully-uploaded": {
"message": "Adicionado com sucesso"
@@ -225,19 +234,19 @@
"message": "Explorar servidores"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' será excluído permanentemente, e não haverá como recuperá-lo."
"message": "'{name}' será **excluído permanentemente**, e não poderá ser recuperado."
},
"app.instance.worlds.delete-world-title": {
"message": "Tem certeza de que deseja excluir este mundo permanentemente?"
"message": "Deseja mesmo excluir este mundo permanentemente?"
},
"app.instance.worlds.filter-modded": {
"message": "Modificado"
},
"app.instance.worlds.filter-offline": {
"message": "Offline"
"message": "Inativo"
},
"app.instance.worlds.filter-online": {
"message": "Online"
"message": "Ativo"
},
"app.instance.worlds.filter-vanilla": {
"message": "Jogo padrão"
@@ -249,13 +258,13 @@
"message": "Nenhum servidor ou mundo adicionado"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' será removido da sua lista, inclusive no jogo, e não haverá como recuperá-lo."
"message": "'{name}' será removido da lista, inclusive no jogo, e não poderá ser recuperado."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) será removido da sua lista, inclusive no jogo, e não haverá como recuperá-lo."
"message": "'{name}' ({address}) será removido da lista, inclusive no jogo, e não poderá ser recuperado."
},
"app.instance.worlds.remove-server-title": {
"message": "Tem certeza de que deseja remover {name}?"
"message": "Deseja mesmo remover {name}?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Buscar {count} mundos..."
@@ -276,7 +285,7 @@
"message": "Pacote de mods necessário"
},
"app.modal.install-to-play.server-requires-mods": {
"message": "Este servidor requer mods para jogar. Clique em Instalar para configurar os arquivos necessários a partir do Modrinth, e iniciar diretamente no servidor."
"message": "Este servidor exige mods para jogar. Clique em instalar para configurar os arquivos necessários através do Modrinth, e então iniciar diretamente no servidor."
},
"app.modal.install-to-play.shared-instance": {
"message": "Instância compartilhada"
@@ -285,7 +294,7 @@
"message": "Instância de servidor compartilhada"
},
"app.modal.install-to-play.view-contents": {
"message": "Ver conteúdos"
"message": "Ver conteúdo"
},
"app.modal.update-to-play.header": {
"message": "Atualize para jogar"
@@ -294,19 +303,19 @@
"message": "Atualização necessária"
},
"app.modal.update-to-play.update-required-description": {
"message": "É necessária uma atualização para jogar {name}. Atualize para a versão mais recente para iniciar o jogo."
"message": "Uma atualização é necessária para jogar {name}. Atualize para a versão mais recente para iniciar o jogo."
},
"app.project.install-button.already-installed": {
"message": "Este projeto já foi instalado"
},
"app.project.install-context.back-to-browse": {
"message": "Voltar ao descobrir"
"message": "Voltar a descobrir"
},
"app.project.install-context.install-content-to-instance": {
"message": "Instalar conteúdo para a instância"
},
"app.settings.developer-mode-enabled": {
"message": "Modo de desenvolvedor ativado."
"message": "Modo desenvolvedor ativado."
},
"app.settings.downloading": {
"message": "Baixando v{version}"
@@ -342,16 +351,16 @@
"message": "Excluir skin"
},
"app.skins.delete-modal.description": {
"message": "Isto permanentemente deletará a skin selecionada. Esta ação não pode ser desfeita."
"message": "Isso excluirá permanentemente a skin. Esta ação não pode ser desfeita."
},
"app.skins.delete-modal.title": {
"message": "Tem certeza de que deseja excluir esta skin?"
"message": "Deseja mesmo excluir esta skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Falha ao ler o arquivo solto."
},
"app.skins.dropped-file-error.title": {
"message": "Erro ao processar o arquivo"
"message": "Erro ao processar arquivo"
},
"app.skins.edit-button": {
"message": "Editar skin"
@@ -360,7 +369,7 @@
"message": "Adicionar skin"
},
"app.skins.modal.add-title": {
"message": "Adicionando skin"
"message": "Adicionando uma skin"
},
"app.skins.modal.arm-style-section": {
"message": "Estilo do braço"
@@ -375,13 +384,13 @@
"message": "Capa"
},
"app.skins.modal.cape-section": {
"message": "Capa"
"message": "Capas"
},
"app.skins.modal.edit-title": {
"message": "Editando skin"
"message": "Editando a skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Primeiro, faça uma edição na skin!"
"message": "Primeiramente, faça uma edição na skin!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Sem capa"
@@ -402,19 +411,19 @@
"message": "Textura"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Primeiro, faça o envio de uma skin!"
"message": "Primeiramente, envie uma skin!"
},
"app.skins.preview.edit-button": {
"message": "Editar skin"
},
"app.skins.previewing-badge": {
"message": "Pré-visualizando"
"message": "Prévia"
},
"app.skins.rate-limit.text": {
"message": "Você está trocando de skin com muita frequência. Os servidores da Mojang bloquearam temporariamente novas solicitações. Aguarde um momento antes de tentar novamente."
"message": "Você está mudando de skin muitas vezes. Os servidores da Mojang bloquearam temporariamente novas solicitações. Aguarde um momento antes de tentar novamente."
},
"app.skins.rate-limit.title": {
"message": "Vá mais devagar!"
"message": "Mais lento!"
},
"app.skins.section.builders-and-biomes": {
"message": "Construtores & Biomas"
@@ -432,10 +441,10 @@
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Orgulho LGBTQIA+ do Modrinth"
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Você recebeu estas skins por doar para uma campanha de arrecadação de fundos do Orgulho LGBTQIA+ do Modrinth durante o Mês do Orgulho."
"message": "Você recebeu estas skins por doar para uma campanha de arrecadação do Modrinth Pride durante o Mês de Orgulho."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
@@ -456,7 +465,7 @@
"message": "Tiny Takeover"
},
"app.skins.sign-in.button": {
"message": "Fazer login"
"message": "Iniciar sessão"
},
"app.skins.sign-in.description": {
"message": "Faça login na sua conta do Minecraft para usar os recursos de gerenciamento de skins do Modrinth app."
@@ -465,13 +474,10 @@
"message": "Modrinth Bot animado"
},
"app.skins.sign-in.title": {
"message": "Por favor, faça login"
"message": "Por favor, inicie sessão"
},
"app.skins.title": {
"message": "Seletor de skin"
},
"app.update-popup.body": {
"message": "O Modrinth App v{version} está pronto para ser instalado! Você pode recarregar para atualizar agora ou a atualização será feita automaticamente ao fechar o Modrinth App."
"message": "Seletor de skins"
},
"app.update-popup.body.download-complete": {
"message": "O Modrinth App v{version} foi baixado. Recarregue para atualizar agora ou a atualização será aplicada automaticamente ao fechar o Modrinth App."
@@ -480,7 +486,7 @@
"message": "O Modrinth App v{version} está disponível. Use seu gerenciador de pacotes para atualizar e obter os recursos e correções mais recentes!"
},
"app.update-popup.body.metered": {
"message": "O Modrinth App v{version} já está disponível! Como você está em uma rede limitada, não o baixamos automaticamente."
"message": "O Modrinth App v{version} já está disponível! Já que sua rede é limitada, não a instalamos automaticamente."
},
"app.update-popup.changelog": {
"message": "Mudanças"
@@ -489,7 +495,7 @@
"message": "Baixar ({size})"
},
"app.update-popup.download-complete": {
"message": "Download concluído"
"message": "Instalação concluída"
},
"app.update-popup.reload": {
"message": "Recarregar"
@@ -503,15 +509,6 @@
"app.update.complete-toast.title": {
"message": "Versão {version} instalada!"
},
"app.update.download-update": {
"message": "Baixar atualização"
},
"app.update.downloading-update": {
"message": "Baixando atualização ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Recarregue para instalar a atualização"
},
"app.world.server-modal.placeholder-address": {
"message": "exemplo.modrinth.gg"
},
@@ -525,22 +522,25 @@
"message": "Ainda não jogado"
},
"app.world.world-item.offline": {
"message": "Offline"
"message": "Inativo"
},
"app.world.world-item.players-online": {
"message": "{count} online"
"message": "{count} ativo(s)"
},
"friends.action.add-friend": {
"message": "Adicionar um amigo"
"message": "Adicionar amigo"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {pedido} other {pedidos}} de amizade"
},
"friends.add-friend.submit": {
"message": "Enviar amizade"
"message": "Enviar pedido de amizade"
},
"friends.add-friend.title": {
"message": "Adicionando amigo"
"message": "Adicionar um amigo"
},
"friends.add-friend.username.description": {
"message": "Pode ser diferente do nome que ele usa no Minecraft!"
"message": "Pode diferir do nome de usuário do Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Insira o nome de usuário..."
@@ -816,13 +816,13 @@
"message": "Servidor de Minecraft"
},
"instance.worlds.cant_connect": {
"message": "Não foi possível se conectar ao servidor"
"message": "Não foi possível conectar ao servidor"
},
"instance.worlds.copy_address": {
"message": "Copiar endereço"
},
"instance.worlds.dont_show_on_home": {
"message": "Não mostrar na tela inicial"
"message": "Não mostrar no início"
},
"instance.worlds.game_already_open": {
"message": "A instância já está aberta"
@@ -239,9 +239,6 @@
"app.settings.tabs.resource-management": {
"message": "Gestão de recursos"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} está pronta para ser instalada! Recarrega para atualizar agora, ou automaticamente quando fechares a Modrinth App."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} acabou de ser transferida. Recarrega para atualizar agora, ou automaticamente quando fechares a Modrinth App."
},
@@ -272,15 +269,6 @@
"app.update.complete-toast.title": {
"message": "Versão {version} foi instalada com sucesso!"
},
"app.update.download-update": {
"message": "Transferir atualização"
},
"app.update.downloading-update": {
"message": "A transferir atualização ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Recarrega para instalar a atualização"
},
"app.world.server-modal.placeholder-address": {
"message": "exemplo.modrinth.gg"
},
@@ -65,9 +65,6 @@
"app.settings.tabs.resource-management": {
"message": "Administrare resurse"
},
"app.update-popup.body": {
"message": "Aplicația Modrinth v{version} este gata de instalat! Reîncărcați pentru a actualiza acum sau automat când închideți aplicația Modrinth."
},
"app.update-popup.body.download-complete": {
"message": "Aplicația Modrinth v{version} a terminat descărcarea. Reîncărcați pentru a actualiza acum sau automat când închideți aplicația Modrinth."
},
@@ -98,15 +95,6 @@
"app.update.complete-toast.title": {
"message": "Versiunea {version} a fost instalată cu succes!"
},
"app.update.download-update": {
"message": "Descarcă actualizarea"
},
"app.update.downloading-update": {
"message": "Se descarcă actualizarea ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Reîncarcă pentru a instala actualizarea"
},
"app.world.server-modal.select-an-option": {
"message": "Selectează o opțiune"
},
+17 -14
View File
@@ -86,6 +86,12 @@
"app.appearance-settings.select-option": {
"message": "Выберите вариант"
},
"app.appearance-settings.show-play-time.description": {
"message": "Показывать время, проведённое в сборке."
},
"app.appearance-settings.show-play-time.title": {
"message": "Время игры"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Показывать кнопку скрытия боковой панели."
},
@@ -143,6 +149,9 @@
"app.browse.server.installing": {
"message": "Установка"
},
"app.content-install.no-compatible-versions": {
"message": "Не найдено доступных версий, совместимых с {compatibilityLabel}. Выберите версию для установки вручную. Зависимости не будут установлены автоматически."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -396,7 +405,7 @@
"message": "Предпросмотр"
},
"app.skins.rate-limit.text": {
"message": "Слишком частая смена скина. Серверы Mojang временно заблокировали дальнейшие запросы. Подождите немного, прежде чем попробовать снова."
"message": "Вы слишком часто меняете скин. Серверы Mojang временно заблокировали дальнейшие запросы. Подождите немного, прежде чем попробовать снова."
},
"app.skins.rate-limit.title": {
"message": "Не так быстро!"
@@ -416,6 +425,12 @@
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Эти скины получены за участие в сборе средств Modrinth Pride в месяц гордости."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
@@ -438,7 +453,7 @@
"message": "Войти"
},
"app.skins.sign-in.description": {
"message": "Чтобы использовать функции управления скинами Modrinth App, войдите в свою учётную запись Minecraft."
"message": "Пожалуйста, войдите в свой аккаунт Minecraft, чтобы воспользоваться функциями управления скинами в приложении Modrinth."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Восторженный Modrinth Bot"
@@ -449,9 +464,6 @@
"app.skins.title": {
"message": "Выбор скина"
},
"app.update-popup.body": {
"message": "Версия Modrinth App {version} готова к установке! Перезапустите приложение, чтобы обновить его, или оно обновится автоматически после закрытия."
},
"app.update-popup.body.download-complete": {
"message": "Скачивание версии Modrinth App {version} завершено. Перезапустите приложение, чтобы обновить его, или оно обновится автоматически после закрытия."
},
@@ -482,15 +494,6 @@
"app.update.complete-toast.title": {
"message": "Версия {version} успешно установлена!"
},
"app.update.download-update": {
"message": "Скачать обновление"
},
"app.update.downloading-update": {
"message": "Скачивание обновления ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Перезапустить и обновить"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
+27 -12
View File
@@ -47,12 +47,21 @@
"app.appearance-settings.color-theme.title": {
"message": "Färgtema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Ändrar sidan som launchern öppnas på."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Hem"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Bibliotek"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Standardlandingssida"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Inaktiverar namnskylten ovanför din spelare på utseende-sidan."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Dölj namnskylt"
},
@@ -68,12 +77,21 @@
"app.appearance-settings.select-option": {
"message": "Välj ett alternativ"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Låter dig växla sidofältet."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Växla sidofält"
},
"app.auth-servers.unreachable.body": {
"message": "Minecrafts autentiseringsservrar kan vara nere just nu. Kontrollera din internetanslutning och försök igen senare."
},
"app.auth-servers.unreachable.header": {
"message": "Kan ej nå autentiseringsservrarna"
},
"app.browse.add-to-an-instance": {
"message": "Lägg till i en instans"
},
"app.browse.add-to-instance": {
"message": "Lägg till i instans"
},
@@ -86,12 +104,18 @@
"app.browse.already-added": {
"message": "Redan tillagd"
},
"app.browse.back-to-instance": {
"message": "Tillbaka till instans"
},
"app.browse.discover-content": {
"message": "Upptäck innehåll"
},
"app.browse.discover-servers": {
"message": "Upptäck servrar"
},
"app.browse.hide-added-servers": {
"message": "Dölj servrar som redan är tillagda"
},
"app.browse.project-type.modpacks": {
"message": "Modpaket"
},
@@ -101,6 +125,9 @@
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Installerar modpaket..."
},
"app.export-modal.description-placeholder": {
"message": "Ange modpaketets beskrivning..."
},
@@ -326,9 +353,6 @@
"app.skins.modal.texture-section": {
"message": "Textur"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} är redo att laddas ner! Ladda om för att uppdatera nu, eller automatiskt när du stänger Modrinth App."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} har laddats ner. Ladda om för att uppdatera nu, eller automatiskt när du stänger Modrinth App."
},
@@ -359,15 +383,6 @@
"app.update.complete-toast.title": {
"message": "Version {version} har installerats!"
},
"app.update.download-update": {
"message": "Ladda ner uppdatering"
},
"app.update.downloading-update": {
"message": "Laddar ner uppdatering ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Ladda om för att installera uppdatering"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -308,9 +308,6 @@
"app.settings.tabs.resource-management": {
"message": "การจัดการทรัพยากร"
},
"app.update-popup.body": {
"message": "Modrinth v{version} พร้อมสำหรับการติดตั้งแล้ว! เปิดโปรแกรมใหม่อีกครั้งเพื่ออัปเดตตอนนี้ หรือจะรออัปเดตอัตโนมัติ ซึ่งจะเกิดขึ้นเมื่อคุณกดปิดโปรแกรม Modrinth"
},
"app.update-popup.body.download-complete": {
"message": "ดาวน์โหลด Modrinth v{version} สำเร็จแล้ว เปิดโปรแกรมใหม่อีกครั้งเพื่ออัปเดตตอนนี้ หรือจะรออัปเดตอัตโนมัติ ซึ่งจะเกิดขึ้นเมื่อคุณกดปิดโปรแกรม Modrinth"
},
@@ -341,15 +338,6 @@
"app.update.complete-toast.title": {
"message": "เวอร์ชั่น {version} ถูกติดตั้งแล้ว"
},
"app.update.download-update": {
"message": "ดาวน์โหลดอัปเดต"
},
"app.update.downloading-update": {
"message": "ดาวน์โหลดอัปเดตไปแล้ว ({percent}%)"
},
"app.update.reload-to-update": {
"message": "รีโหลดเพื่อติดตั้งอัปเดต"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
+42 -36
View File
@@ -86,6 +86,12 @@
"app.appearance-settings.select-option": {
"message": "Bir seçenek seçin"
},
"app.appearance-settings.show-play-time.description": {
"message": "Bir oyun oturumunda ne kadar zaman geçirdiğinizi gösterir."
},
"app.appearance-settings.show-play-time.title": {
"message": "Oynama Süresini Göster"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Kenar çubuğunu ayarlama özelliğini aktifleştirir."
},
@@ -143,6 +149,9 @@
"app.browse.server.installing": {
"message": "Yükleniyor"
},
"app.content-install.no-compatible-versions": {
"message": "Kullanılabilir sürümler {compatibilityLabel} ile eşleşmiyor. Yine de kurulacak bir sürüm seçin. Bağımlılıklar otomatik olarak yüklenmeyecektir."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -165,7 +174,7 @@
"message": "Mod Paketi Adı"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Modpaketi adı"
"message": "Mod paketi adı"
},
"app.export-modal.select-files-label": {
"message": "Bu dışa aktarmaya hangi dosyaların dahil edileceğini yapılandırma"
@@ -183,10 +192,10 @@
"message": "Bu eylem geri alınamaz"
},
"app.instance.confirm-delete.delete-button": {
"message": "Kurulumu sil"
"message": "Oturumu sil"
},
"app.instance.confirm-delete.header": {
"message": "Kurulumu sil"
"message": "Oturumu sil"
},
"app.instance.modpack-already-installed.body": {
"message": "Bu mod paketi zaten <bold>{instanceName}</bold> kurulumunda yüklü. Kopyalamak istediğinizden emin misiniz?"
@@ -273,7 +282,7 @@
"message": "Yükle"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural,one {#mod}other {#modlar}}"
"message": "{count, plural, one {#mod} other {#mods}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Gerekli mod paketi"
@@ -336,7 +345,7 @@
"message": "Skin ekle"
},
"app.skins.add-button.drag-and-drop": {
"message": "Sürükle bırak"
"message": "Sürükle ve bırak"
},
"app.skins.apply-button": {
"message": "Uygula"
@@ -423,7 +432,7 @@
"message": "İnşaatçılar & Biyomlar"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
"message": "Gökyüzünü kovala"
},
"app.skins.section.default-skins": {
"message": "Varsayılan skinler"
@@ -431,23 +440,32 @@
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Onuru"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Bu skinleri, Onur Ayı boyunca Modrinth'in düzenlediği bir bağış kampanyasına yaptığınız bağış karşılığında aldınız."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
"message": "Kaos Dağları"
},
"app.skins.section.saved-skins": {
"message": "Kaydedilen Skinler"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
"message": "Yürüyen Kahraman"
},
"app.skins.section.the-copper-age": {
"message": "The Copper Age"
"message": "Bakır Çağı"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
"message": "Bahçe Uyanıyor"
},
"app.skins.section.tiny-takeover": {
"message": "Tiny Takeover"
"message": "Küçük Devralma"
},
"app.skins.sign-in.button": {
"message": "Giriş Yap"
@@ -464,11 +482,8 @@
"app.skins.title": {
"message": "Skin seçici"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} güncellemesi indirilmeye hazır! Hemen güncellemek için yeniden başlatın veya Modrinth Appi kapattığınızda otomatik olarak güncellenecek."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} indirildi. Güncellemek için yeniden başlatın veya Modrinth Appi kapattığınızda otomatik olarak güncellenecek."
"message": "Modrinth App v{version} indirildi. Güncellemek için yeniden başlatın veya Modrinth Appi kapattığınızda otomatik olarak güncellenecektir."
},
"app.update-popup.body.linux": {
"message": "Modrinth App v{version} yayımlandı. En yeni özellikler ve hata düzeltmeleri için paket yöneticin üzerinden güncelle!"
@@ -492,20 +507,11 @@
"message": "Güncelleme mevcut"
},
"app.update.complete-toast.text": {
"message": "Değişiklikleri görüntülemek için buraya tıklayın."
"message": "Yama notlarını görüntülemek için buraya tıklayın."
},
"app.update.complete-toast.title": {
"message": "{version} sürümü başarıyla kuruldu!"
},
"app.update.download-update": {
"message": "Güncellemeyi indir"
},
"app.update.downloading-update": {
"message": "Güncelleme indiriliyor (%{percent})"
},
"app.update.reload-to-update": {
"message": "Güncellemeyi kurmak için yeniden başlatın"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -519,7 +525,7 @@
"message": "Henüz oynanmadı"
},
"app.world.world-item.offline": {
"message": "Çevrim dışı"
"message": "Çevrimdışı"
},
"app.world.world-item.players-online": {
"message": "{count} çevrimiçi"
@@ -555,16 +561,16 @@
"message": "Arkadaşlık isteği gönderildi"
},
"friends.friend.view-profile": {
"message": "Profili gör"
"message": "Profili görüntüle"
},
"friends.heading": {
"message": "Arkadaşlar"
},
"friends.heading.active": {
"message": "Aktif"
"message": "Çevrimiçi"
},
"friends.heading.offline": {
"message": "Çevrim dışı"
"message": "Çevrimdışı"
},
"friends.heading.online": {
"message": "Çevrim içi"
@@ -630,7 +636,7 @@
"message": "Adres"
},
"instance.server-modal.name": {
"message": "Ad"
"message": "Sunucu Adı"
},
"instance.server-modal.placeholder-name": {
"message": "Minecraft Sunucusu"
@@ -657,7 +663,7 @@
"message": "Çoğalt"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "İndirilirken çoğaltılamaz."
"message": "Kurulurken çoğaltılamaz."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "Profili çoğalt"
@@ -696,7 +702,7 @@
"message": "Başlatma kancaları"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "Özel başlatma kancası"
"message": "Özel başlatma kancaları"
},
"instance.settings.tabs.hooks.description": {
"message": "Kancalar gelişmiş kullanıcıların oyunu başlattıktan önce ve sonra belirli sistem komutları çalıştırmasını sağlar."
@@ -723,13 +729,13 @@
"message": "Oyun başlatma kancaları"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Sarmalayıcı"
"message": "Paketleyici"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "Minecraft'ı başlatmak için sarmalayıcı komutu."
"message": "Minecraft'ı başlatmak için paketleyici komutu."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Sarmalayıcı komutu girin..."
"message": "Paketleyici komutu girin..."
},
"instance.settings.tabs.installation": {
"message": "Kurulum"
@@ -837,7 +843,7 @@
"message": "Yalnızca Minecraft Alpha 1.0.5 veya daha yüksek sunuculara hızlıca girebilirsin"
},
"instance.worlds.no_singleplayer_quick_play": {
"message": "Yalnızca Minecraft 1.20 veya daha yüksek sürümlerde tek oyunculu dünyalara doğrudan girebilirsin"
"message": "Yalnızca Minecraft 1.20+ sürümlerde tek oyunculu dünyalara doğrudan girebilirsin"
},
"instance.worlds.play_instance": {
"message": "Kurulumu oyna"
+15 -12
View File
@@ -86,6 +86,12 @@
"app.appearance-settings.select-option": {
"message": "Виберіть опцію"
},
"app.appearance-settings.show-play-time.description": {
"message": "Показує, скільки часу ви провели в цьому профілі."
},
"app.appearance-settings.show-play-time.title": {
"message": "Час гри"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Умикає можливість перемикання бокової панелі."
},
@@ -419,6 +425,15 @@
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Ви отримали ці скіни за участь у благодійній акції Modrinth Pride під час місяця прайду."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
@@ -452,9 +467,6 @@
"app.skins.title": {
"message": "Вибір скіну"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} готовий до встановлення! Перезапустіть, щоб оновити зараз. Або, оновлення буде здійснено автоматично, коли закриєте застосунок Modrinth."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} завантажено. Перезапустіть зараз, щоб оновити його, або це відбудеться автоматично після закриття Modrinth App."
},
@@ -485,15 +497,6 @@
"app.update.complete-toast.title": {
"message": "Версію {version} успішно встановлено!"
},
"app.update.download-update": {
"message": "Завантажити оновлення"
},
"app.update.downloading-update": {
"message": "Завантаження оновлення ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Перезавантажте, щоб установити оновлення"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
+9 -21
View File
@@ -86,6 +86,12 @@
"app.appearance-settings.select-option": {
"message": "Chọn một tuỳ chọn"
},
"app.appearance-settings.show-play-time.description": {
"message": "Hiển thị thời gian bạn đã dành để chơi bằng một cấu hình."
},
"app.appearance-settings.show-play-time.title": {
"message": "Hiện thời gian chơi"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Bật tính năng ẩn hiển thị thanh menu bên."
},
@@ -272,9 +278,6 @@
"app.modal.install-to-play.install-button": {
"message": "Tải xuống"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} other {# mods}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Yêu cầu modpack"
},
@@ -438,7 +441,7 @@
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Bạn đã được tặng những skin này sau khi quyên góp cho Modrinth trong tháng Lẩu Gà Bình Thuận thế giới."
"message": "Bạn đã được tặng những skin này sau khi quyên góp cho Modrinth trong tháng tự hào LGBT thế giới."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
@@ -462,7 +465,7 @@
"message": "Đăng nhập"
},
"app.skins.sign-in.description": {
"message": "Đặng nhập vào mội tài khoản Minecraft để sử dụng trình sửa Skin trong ứng dụng này."
"message": "Đặng nhập vào mội tài khoản Minecraft để đổi skin Skin trong ứng dụng này."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Bot phấn khích"
@@ -471,10 +474,7 @@
"message": "Đăng nhập đã"
},
"app.skins.title": {
"message": "Trình sửa skin"
},
"app.update-popup.body": {
"message": "Phiên bản v{version} của Modrinth đã được chuẩn bị để cài đặt! Khởi động lại ứng dụng để cập nhật ngay bây giờ, hoặc cập nhật tự động khi bạn đóng Modrinth."
"message": "Đổi skin"
},
"app.update-popup.body.download-complete": {
"message": "Phiên bản v{version} của Modrinth đã sẵn sàng để có thể cài đặt. Khởi động lại ứng dụng để cập nhật ngay bây giờ, hoặc tự động cập nhật sau khi bạn tắt Modrinth."
@@ -506,15 +506,6 @@
"app.update.complete-toast.title": {
"message": "Phiên bản {version} đã được cài đặt thành công!"
},
"app.update.download-update": {
"message": "Tải về bản cập nhật"
},
"app.update.downloading-update": {
"message": "Đang tải xuống bản cập nhật ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Hãy khởi động lại để cài đặt bản cập nhật"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -536,9 +527,6 @@
"friends.action.add-friend": {
"message": "Thêm một người bạn"
},
"friends.action.view-friend-requests": {
"message": "{count} yêu cầu kết bạn {count, plural, one {request} other {requests}}"
},
"friends.add-friend.submit": {
"message": "Gửi yêu cầu kết bạn"
},
+21 -18
View File
@@ -86,6 +86,12 @@
"app.appearance-settings.select-option": {
"message": "选择一个选项"
},
"app.appearance-settings.show-play-time.description": {
"message": "显示你在每个实例中游玩的时间。"
},
"app.appearance-settings.show-play-time.title": {
"message": "显示总游玩时间"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "启用切换侧边栏的功能。"
},
@@ -120,7 +126,7 @@
"message": "已添加"
},
"app.browse.already-added": {
"message": "已添加"
"message": "已添加"
},
"app.browse.back-to-instance": {
"message": "返回实例"
@@ -143,6 +149,9 @@
"app.browse.server.installing": {
"message": "安装中"
},
"app.content-install.no-compatible-versions": {
"message": "没有与 {compatibilityLabel} 匹配的可用版本。如果仍要安装,请选择一个版本。依赖项将不会自动安装。"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -411,7 +420,7 @@
"message": "建造者与生物群系"
},
"app.skins.section.chase-the-skies": {
"message": "追逐苍穹"
"message": "追逐天空"
},
"app.skins.section.default-skins": {
"message": "默认皮肤"
@@ -422,23 +431,29 @@
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth 精选"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "你在骄傲月期间为 Modrinth 的骄傲月筹款活动捐款,因此获得了这些皮肤。"
},
"app.skins.section.mounts-of-mayhem": {
"message": "暴乱坐骑"
"message": "群骑纷争"
},
"app.skins.section.saved-skins": {
"message": "已保存皮肤"
},
"app.skins.section.striding-hero": {
"message": "阔步英雄"
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "铜器时代"
},
"app.skins.section.the-garden-awakens": {
"message": "花园苏醒"
"message": "苍园觉醒"
},
"app.skins.section.tiny-takeover": {
"message": "微小占领"
"message": "小鬼当家"
},
"app.skins.sign-in.button": {
"message": "登录"
@@ -455,9 +470,6 @@
"app.skins.title": {
"message": "皮肤选择器"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} 更新已就绪!立即重启更新,或退出时自动安装。"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} 更新已下载完成!立即重启更新,或退出时自动安装。"
},
@@ -488,15 +500,6 @@
"app.update.complete-toast.title": {
"message": "版本 {version} 已成功安装!"
},
"app.update.download-update": {
"message": "下载更新"
},
"app.update.downloading-update": {
"message": "下载更新中({percent}%"
},
"app.update.reload-to-update": {
"message": "重启以安装更新"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
+83 -11
View File
@@ -86,6 +86,12 @@
"app.appearance-settings.select-option": {
"message": "選擇選項"
},
"app.appearance-settings.show-play-time.description": {
"message": "顯示您在該實例的遊玩時間"
},
"app.appearance-settings.show-play-time.title": {
"message": "顯示遊玩時間"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "啟用切換側邊欄的功能。"
},
@@ -320,8 +326,83 @@
"app.settings.tabs.resource-management": {
"message": "資源管理"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} 已準備好安裝!立即重新載入以更新,或在關閉 Modrinth App 時自動更新。"
"app.skins.add-button": {
"message": "新增皮膚"
},
"app.skins.apply-button": {
"message": "確認"
},
"app.skins.delete-button": {
"message": "刪除皮膚"
},
"app.skins.delete-modal.title": {
"message": "你確定要刪除這個皮膚嗎"
},
"app.skins.edit-button": {
"message": "編輯皮膚"
},
"app.skins.modal.add-skin-button": {
"message": "新增皮膚"
},
"app.skins.modal.add-title": {
"message": "正在新增皮膚"
},
"app.skins.modal.cape-fallback-name": {
"message": "披風"
},
"app.skins.modal.cape-section": {
"message": "披風"
},
"app.skins.modal.edit-title": {
"message": "編輯皮膚"
},
"app.skins.modal.no-cape-tooltip": {
"message": "無披風"
},
"app.skins.modal.none-cape-option": {
"message": "無"
},
"app.skins.modal.replace-texture-button": {
"message": "覆蓋材質包"
},
"app.skins.modal.save-skin-button": {
"message": "儲存皮膚"
},
"app.skins.modal.saving-tooltip": {
"message": "正在儲存"
},
"app.skins.modal.texture-section": {
"message": "材質包"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "馬上新增一個皮膚"
},
"app.skins.preview.edit-button": {
"message": "編輯皮膚"
},
"app.skins.previewing-badge": {
"message": "正在預覽"
},
"app.skins.rate-limit.title": {
"message": "慢一點"
},
"app.skins.section.default-skins": {
"message": "預設皮膚"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.saved-skins": {
"message": "皮膚已儲存"
},
"app.skins.sign-in.button": {
"message": "登入"
},
"app.skins.sign-in.title": {
"message": "請登入"
},
"app.skins.title": {
"message": "皮膚選擇"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} 已完成下載!立即重新載入以更新,或在關閉 Modrinth App 時自動更新。"
@@ -353,15 +434,6 @@
"app.update.complete-toast.title": {
"message": "版本 {version} 已成功安裝!"
},
"app.update.download-update": {
"message": "下載更新"
},
"app.update.downloading-update": {
"message": "正在下載更新 ({percent}%)"
},
"app.update.reload-to-update": {
"message": "重新載入即可安裝更新"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -0,0 +1,38 @@
<script setup lang="ts">
import {
injectModrinthClient,
injectModrinthServerContext,
ServersManageAccessPage,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
const client = injectModrinthClient()
const { serverId } = injectModrinthServerContext()
const queryClient = useQueryClient()
try {
await Promise.all([
queryClient.ensureQueryData({
queryKey: ['servers', 'users', 'v1', serverId],
queryFn: () => client.archon.server_users_v1.list(serverId),
staleTime: 30_000,
}),
queryClient.ensureQueryData({
queryKey: ['servers', 'v1', 'detail', serverId],
queryFn: () => client.archon.servers_v1.get(serverId),
staleTime: 30_000,
}),
])
} catch {
// Let mounted layouts' useQuery surface errors; do not fail route setup.
}
function userProfileLink(username: string) {
return () => openUrl(`https://modrinth.com/user/${encodeURIComponent(username)}`)
}
</script>
<template>
<ServersManageAccessPage :user-profile-link="userProfileLink" />
</template>
@@ -1,7 +1,8 @@
import Access from './Access.vue'
import Backups from './Backups.vue'
import Content from './Content.vue'
import Files from './Files.vue'
import Index from './Index.vue'
import Overview from './Overview.vue'
export { Backups, Content, Files, Index, Overview }
export { Access, Backups, Content, Files, Index, Overview }
@@ -1227,6 +1227,15 @@ watch(
},
)
watch(
() => props.instance?.preferred_update_channel,
async (newValue, oldValue) => {
if (newValue !== oldValue) {
await initProjects('must_revalidate')
}
},
)
onUnmounted(() => {
isUnmounted = true
removeBeforeEach()
@@ -0,0 +1,171 @@
import { computed, ref } from 'vue'
export const APP_UPDATE_POPUP_DELAY_MS = 24 * 60 * 60 * 1000
const UPDATE_PROMPT_STORAGE_KEY = 'modrinth-app-update-prompt-state'
export interface AppUpdate {
rid: number
version: string
currentVersion?: string
}
interface UpdatePromptState {
version: string
stage: AppUpdatePromptStage
actionableSince: number
lastUserActionAt?: number
popupShownAt?: number
}
export type AppUpdatePromptStage = 'available' | 'downloaded'
interface AppUpdateActions {
download?: () => Promise<void> | void
install?: () => Promise<void> | void
changelog?: () => Promise<void> | void
}
const progress = ref(0)
const metered = ref(true)
const finishedDownloading = ref(false)
const downloading = ref(false)
const restarting = ref(false)
const availableUpdate = ref<AppUpdate | null>(null)
const updateSize = ref<number | null>(null)
const updatesEnabled = ref(true)
let actions: AppUpdateActions = {}
function getCurrentAppUpdatePromptStage(): AppUpdatePromptStage {
return finishedDownloading.value ? 'downloaded' : 'available'
}
export const appUpdateState = {
progress,
metered,
finishedDownloading,
downloading,
restarting,
availableUpdate,
updateSize,
updatesEnabled,
downloadProgress: computed(() => progress.value),
downloadPercent: computed(() => Math.trunc(progress.value * 100)),
isVisible: computed(() => !!availableUpdate.value && !restarting.value && updatesEnabled.value),
}
function readPromptState(): UpdatePromptState | null {
try {
const raw = localStorage.getItem(UPDATE_PROMPT_STORAGE_KEY)
if (!raw) {
return null
}
const parsed = JSON.parse(raw) as Partial<UpdatePromptState>
if (!parsed.version || typeof parsed.actionableSince !== 'number') {
return null
}
return {
...parsed,
stage: parsed.stage ?? 'available',
} as UpdatePromptState
} catch {
return null
}
}
function writePromptState(state: UpdatePromptState): void {
try {
localStorage.setItem(UPDATE_PROMPT_STORAGE_KEY, JSON.stringify(state))
} catch (error) {
console.warn('Failed to persist update prompt state:', error)
}
}
export function markAppUpdateActionable(
version: string,
stage: AppUpdatePromptStage = 'available',
now = Date.now(),
): void {
const existing = readPromptState()
if (existing?.version === version && existing.stage === stage) {
return
}
writePromptState({
version,
stage,
actionableSince: now,
})
}
export function recordAppUpdateUserAction(
version = availableUpdate.value?.version,
stage: AppUpdatePromptStage = getCurrentAppUpdatePromptStage(),
): void {
if (!version) {
return
}
const now = Date.now()
const existing = readPromptState()
const isSamePrompt = existing?.version === version && existing.stage === stage
writePromptState({
version,
stage,
actionableSince: isSamePrompt ? existing.actionableSince : now,
lastUserActionAt: now,
popupShownAt: isSamePrompt ? existing.popupShownAt : undefined,
})
}
export function markAppUpdatePopupShown(
version: string,
stage: AppUpdatePromptStage = 'available',
now = Date.now(),
): void {
const existing = readPromptState()
const isSamePrompt = existing?.version === version && existing.stage === stage
writePromptState({
version,
stage,
actionableSince: isSamePrompt ? existing.actionableSince : now,
lastUserActionAt: isSamePrompt ? existing.lastUserActionAt : undefined,
popupShownAt: now,
})
}
export function getNextAppUpdatePopupTime(
version: string,
stage: AppUpdatePromptStage = 'available',
): number | null {
const existing = readPromptState()
if (existing?.version !== version || existing.stage !== stage || existing.popupShownAt) {
return null
}
return (
Math.max(existing.actionableSince, existing.lastUserActionAt ?? 0) + APP_UPDATE_POPUP_DELAY_MS
)
}
export function setAppUpdateActions(nextActions: AppUpdateActions): void {
actions = nextActions
}
export async function downloadAvailableAppUpdate(): Promise<void> {
recordAppUpdateUserAction(undefined, 'available')
await actions.download?.()
}
export async function installAvailableAppUpdate(): Promise<void> {
recordAppUpdateUserAction(undefined, 'downloaded')
await actions.install?.()
}
export async function openAppUpdateChangelog(): Promise<void> {
recordAppUpdateUserAction()
await actions.changelog?.()
}
@@ -1,6 +1,6 @@
import type { Labrinth } from '@modrinth/api-client'
import type { ContentInstallInstance, ContentInstallProjectInfo, ContentItem } from '@modrinth/ui'
import { createContext } from '@modrinth/ui'
import { createContext, defineMessage, useVIntl } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { openUrl } from '@tauri-apps/plugin-opener'
import dayjs from 'dayjs'
@@ -34,7 +34,7 @@ import {
} from '@/store/install.js'
interface ModalRef {
show: () => void
show: (initialVersionId?: string) => void
hide: () => void
}
@@ -42,19 +42,14 @@ interface ModpackAlreadyInstalledModalRef {
show: (instanceName: string, instancePath: string) => void
}
interface IncompatibilityWarningModalRef {
show: (
instance: GameInstance,
project: Labrinth.Projects.v2.Project,
versions: Labrinth.Versions.v2.Version[],
version: Labrinth.Versions.v2.Version,
callback: (versionId?: string) => void,
) => void
}
const LOADER_ORDER = ['vanilla', 'fabric', 'quilt', 'neoforge', 'forge']
const SUPPORTED_LOADERS: Set<string> = new Set(['vanilla', 'forge', 'fabric', 'quilt', 'neoforge'])
const VANILLA_COMPATIBLE_LOADERS: Set<string> = new Set(['minecraft', 'datapack'])
const noCompatibleVersionsMessage = defineMessage({
id: 'app.content-install.no-compatible-versions',
defaultMessage:
'No available versions match {compatibilityLabel}. Select a version to install anyway. Dependencies will not be installed automatically.',
})
function sortLoaders(loaders: string[]): string[] {
return loaders.slice().sort((a, b) => {
@@ -91,7 +86,17 @@ export interface ContentInstallContext {
setModpackAlreadyInstalledModal: (ref: ModpackAlreadyInstalledModalRef) => void
handleModpackDuplicateCreateAnyway: () => Promise<void>
handleModpackDuplicateGoToInstance: (instancePath: string) => void
setIncompatibilityWarningModal: (ref: IncompatibilityWarningModalRef) => void
setIncompatibilityWarningModal: (ref: ModalRef) => void
incompatibilityWarningVersions: Ref<Labrinth.Versions.v2.Version[]>
incompatibilityWarningCurrentGameVersion: Ref<string>
incompatibilityWarningCurrentLoader: Ref<string>
incompatibilityWarningProjectType: Ref<string | undefined>
incompatibilityWarningProjectIconUrl: Ref<string | undefined>
incompatibilityWarningProjectName: Ref<string | undefined>
incompatibilityWarningMessage: Ref<string | undefined>
incompatibilityWarningInstalling: Ref<boolean>
handleIncompatibilityWarningInstall: (version: Labrinth.Versions.v2.Version) => Promise<void>
handleIncompatibilityWarningCancel: () => void
install: (
projectId: string,
versionId?: string | null,
@@ -113,6 +118,7 @@ export function createContentInstall(opts: {
router: Router
handleError: (err: unknown) => void
}): ContentInstallContext {
const { formatMessage } = useVIntl()
const instances = ref<ContentInstallInstance[]>([])
const compatibleLoaders = ref<string[]>([])
const gameVersions = ref<string[]>([])
@@ -124,6 +130,14 @@ export function createContentInstall(opts: {
const projectInfo = ref<ContentInstallProjectInfo | null>(null)
const installingItems = ref<Map<string, ContentItem[]>>(new Map())
const incompatibilityWarningVersions = ref<Labrinth.Versions.v2.Version[]>([])
const incompatibilityWarningCurrentGameVersion = ref('')
const incompatibilityWarningCurrentLoader = ref('')
const incompatibilityWarningProjectType = ref<string | undefined>(undefined)
const incompatibilityWarningProjectIconUrl = ref<string | undefined>(undefined)
const incompatibilityWarningProjectName = ref<string | undefined>(undefined)
const incompatibilityWarningMessage = ref<string | undefined>(undefined)
const incompatibilityWarningInstalling = ref(false)
function addInstallingItem(
instancePath: string,
@@ -239,11 +253,15 @@ export function createContentInstall(opts: {
let modalRef: ModalRef | null = null
let modpackAlreadyInstalledModalRef: ModpackAlreadyInstalledModalRef | null = null
let incompatibilityWarningModalRef: IncompatibilityWarningModalRef | null = null
let incompatibilityWarningModalRef: ModalRef | null = null
let currentProject: Labrinth.Projects.v2.Project | null = null
let currentVersions: Labrinth.Versions.v2.Version[] = []
let currentCallback: (versionId?: string) => void = () => {}
let profileMap: Record<string, GameInstance> = {}
let incompatibilityWarningInstance: GameInstance | null = null
let incompatibilityWarningProject: Labrinth.Projects.v2.Project | null = null
let incompatibilityWarningCallback: (versionId?: string) => void = () => {}
let incompatibilityWarningInstalled = false
let pendingModpackInstall: {
project: Labrinth.Projects.v2.Project
@@ -410,15 +428,35 @@ export function createContentInstall(opts: {
async function handleInstallToInstance(instance: ContentInstallInstance) {
const profile = profileMap[instance.id]
const storeInstance = instances.value.find((i) => i.id === instance.id)
if (storeInstance) storeInstance.installing = true
if (!currentProject || !profile) {
opts.handleError('No project or instance found')
return
}
const version = findPreferredVersion(currentVersions, currentProject, profile)
if (!version) {
if (storeInstance) storeInstance.installing = false
opts.handleError('No compatible version found')
if (currentVersions.length > 0 && incompatibilityWarningModalRef) {
const onIncompatibleInstall = (versionId?: string) => {
if (versionId && storeInstance) {
storeInstance.installed = true
}
currentCallback(versionId)
}
await showIncompatibilityWarning(
profile,
currentProject,
currentVersions,
currentVersions[0],
onIncompatibleInstall,
)
} else {
opts.handleError('No version found')
}
return
}
if (storeInstance) storeInstance.installing = true
const installedProjectIds: string[] = []
if (currentProject) {
addInstallingItem(instance.id, currentProject, version)
@@ -458,6 +496,73 @@ export function createContentInstall(opts: {
}
}
async function showIncompatibilityWarning(
instance: GameInstance,
project: Labrinth.Projects.v2.Project,
versions: Labrinth.Versions.v2.Version[],
version: Labrinth.Versions.v2.Version,
callback: (versionId?: string) => void,
) {
incompatibilityWarningInstance = instance
incompatibilityWarningProject = project
incompatibilityWarningCallback = callback
incompatibilityWarningInstalled = false
incompatibilityWarningInstalling.value = false
incompatibilityWarningVersions.value = versions
incompatibilityWarningCurrentGameVersion.value = instance.game_version ?? ''
incompatibilityWarningCurrentLoader.value = instance.loader ?? ''
incompatibilityWarningProjectType.value = project.project_type
incompatibilityWarningProjectIconUrl.value = project.icon_url ?? undefined
incompatibilityWarningProjectName.value = project.title
const compatibilityLabel =
project.project_type === 'resourcepack' || project.project_type === 'datapack'
? (instance.game_version ?? '')
: `${instance.loader ?? ''} ${instance.game_version ?? ''}`.trim()
incompatibilityWarningMessage.value = formatMessage(noCompatibleVersionsMessage, {
compatibilityLabel,
})
await nextTick()
incompatibilityWarningModalRef?.show(version.id)
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
}
async function handleIncompatibilityWarningInstall(version: Labrinth.Versions.v2.Version) {
if (!incompatibilityWarningInstance || !incompatibilityWarningProject) return
incompatibilityWarningInstalling.value = true
try {
await add_project_from_version(incompatibilityWarningInstance.path, version.id, 'standalone')
} catch (err) {
opts.handleError(err)
incompatibilityWarningInstalling.value = false
return
}
incompatibilityWarningInstalling.value = false
incompatibilityWarningInstalled = true
incompatibilityWarningCallback(version.id)
incompatibilityWarningModalRef?.hide()
trackEvent('ProjectInstall', {
loader: incompatibilityWarningInstance.loader,
game_version: incompatibilityWarningInstance.game_version,
id: incompatibilityWarningProject.id,
version_id: version.id,
project_type: incompatibilityWarningProject.project_type,
title: incompatibilityWarningProject.title,
source: 'ProjectIncompatibilityWarningModal',
})
}
function handleIncompatibilityWarningCancel() {
if (!incompatibilityWarningInstalled) {
incompatibilityWarningCallback()
}
incompatibilityWarningInstalled = false
}
async function handleCreateAndInstall(data: {
name: string
iconPath: string | null
@@ -614,7 +719,7 @@ export function createContentInstall(opts: {
removeInstallingItems(instancePath, installedProjectIds)
}
} else {
incompatibilityWarningModalRef?.show(instance, project, projectVersions, version, callback)
await showIncompatibilityWarning(instance, project, projectVersions, version, callback)
}
} else {
let versions = (
@@ -668,9 +773,19 @@ export function createContentInstall(opts: {
pendingModpackInstall = null
opts.router.push(`/instance/${encodeURIComponent(instancePath)}`)
},
setIncompatibilityWarningModal(ref: IncompatibilityWarningModalRef) {
setIncompatibilityWarningModal(ref: ModalRef) {
incompatibilityWarningModalRef = ref
},
incompatibilityWarningVersions,
incompatibilityWarningCurrentGameVersion,
incompatibilityWarningCurrentLoader,
incompatibilityWarningProjectType,
incompatibilityWarningProjectIconUrl,
incompatibilityWarningProjectName,
incompatibilityWarningMessage,
incompatibilityWarningInstalling,
handleIncompatibilityWarningInstall,
handleIncompatibilityWarningCancel,
install,
installingItems,
}
+2
View File
@@ -1,6 +1,7 @@
import type { AbstractPopupNotificationManager, AbstractWebNotificationManager } from '@modrinth/ui'
import { setupCreationModal } from './setup/creation-modal'
import { setupFileDropProvider } from './setup/file-drop'
import { setupFilePickerProvider } from './setup/file-picker'
import { setupInstanceImportProvider } from './setup/instance-import'
import { setupTagsProvider } from './setup/tags'
@@ -10,6 +11,7 @@ export function setupProviders(
popupNotificationManager: AbstractPopupNotificationManager,
) {
setupTagsProvider(notificationManager)
setupFileDropProvider()
setupFilePickerProvider()
setupInstanceImportProvider(notificationManager)
@@ -0,0 +1,62 @@
import { provideFileDrop } from '@modrinth/ui'
import { invoke } from '@tauri-apps/api/core'
import type { DragDropEvent } from '@tauri-apps/api/webview'
import { getCurrentWebview } from '@tauri-apps/api/webview'
function getFileName(path: string) {
return path.split(/[\\/]/).pop() || 'file'
}
function toLogicalPosition(position: { x: number; y: number }) {
const scale = window.devicePixelRatio || 1
return {
x: position.x / scale,
y: position.y / scale,
}
}
async function readDraggedFile(path: string) {
const data = await invoke<number[]>('plugin:files|file_read_dragged_file', { path })
return new Uint8Array(data)
}
export function setupFileDropProvider() {
let nativeFileDropPaths: string[] = []
provideFileDrop({
async listenNativeFileDrop(handler) {
return await getCurrentWebview().onDragDropEvent((event: { payload: DragDropEvent }) => {
const payload = event.payload
if (payload.type === 'leave') {
nativeFileDropPaths = []
void handler({
type: 'leave',
paths: [],
position: { x: 0, y: 0 },
})
return
}
if (payload.type === 'enter' || payload.type === 'drop') {
nativeFileDropPaths = payload.paths
}
void handler({
type: payload.type,
paths: nativeFileDropPaths,
position: toLogicalPosition(payload.position),
})
if (payload.type === 'drop') {
nativeFileDropPaths = []
}
})
},
async createFilesFromNativePaths(paths) {
return await Promise.all(
paths.map(async (path) => new File([await readDraggedFile(path)], getFileName(path))),
)
},
})
}
@@ -26,7 +26,7 @@ export function setupFilePickerProvider() {
const file = await createFileFromPath(path, 'icon')
return { file, path, previewUrl: convertFileSrc(path) }
},
async pickModpackFile() {
async pickModpackFile(options) {
const result = await open({
multiple: false,
filters: [{ name: 'Modpack', extensions: ['mrpack'] }],
@@ -34,12 +34,19 @@ export function setupFilePickerProvider() {
if (!result) return null
const path = result.path ?? result
if (!path) return null
const file = await createFileFromPath(
if (options?.readFile === false) {
// Instance imports stream from the native path, keeping large packs out of JS memory.
return { path, previewUrl: '' }
}
return {
file: await createFileFromPath(
path,
'modpack.mrpack',
'application/x-modrinth-modpack+zip',
),
path,
'modpack.mrpack',
'application/x-modrinth-modpack+zip',
)
return { file, path, previewUrl: '' }
previewUrl: '',
}
},
})
}
+8
View File
@@ -73,6 +73,14 @@ export default new createRouter({
breadcrumb: [{ name: '?Server' }],
},
},
{
path: 'access',
name: 'ServerManageAccess',
component: Hosting.Access,
meta: {
breadcrumb: [{ name: '?Server' }],
},
},
],
},
{
+5 -1
View File
@@ -270,7 +270,11 @@ fn main() {
.plugin(
"files",
InlinedPlugin::new()
.commands(&["file_extract_zip", "file_save_as"])
.commands(&[
"file_extract_zip",
"file_save_as",
"file_read_dragged_file",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
+6
View File
@@ -42,6 +42,12 @@
{
"url": "https://api.purpurmc.org/*"
},
{
"url": "http://localhost:8000/*"
},
{
"url": "http://127.0.0.1:8000/*"
},
{
"url": "http://*.taila228c5.ts.net/*"
},
+14
View File
@@ -11,6 +11,7 @@ pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
.invoke_handler(tauri::generate_handler![
file_extract_zip,
file_save_as,
file_read_dragged_file,
])
.build()
}
@@ -21,6 +22,19 @@ pub struct ExtractDryRunResult {
conflicting_files: Vec<String>,
}
#[tauri::command]
pub async fn file_read_dragged_file(path: String) -> Result<Vec<u8>> {
let metadata = tokio::fs::metadata(&path).await?;
if !metadata.is_file() {
return Err(theseus::Error::from(theseus::ErrorKind::OtherError(
"Dropped path is not a file".to_string(),
))
.into());
}
Ok(tokio::fs::read(path).await?)
}
#[tauri::command]
pub async fn file_extract_zip(
instance_path: &str,
+6
View File
@@ -385,6 +385,7 @@ pub struct EditProfile {
with = "serde_with::rust::double_option"
)]
pub linked_data: Option<Option<LinkedData>>,
pub preferred_update_channel: Option<ReleaseChannel>,
#[serde(
default,
@@ -449,6 +450,11 @@ pub async fn profile_edit(path: &str, edit_profile: EditProfile) -> Result<()> {
if let Some(linked_data) = edit_profile.linked_data.clone() {
prof.linked_data = linked_data;
}
if let Some(preferred_update_channel) =
edit_profile.preferred_update_channel
{
prof.preferred_update_channel = preferred_update_channel;
}
if let Some(groups) = edit_profile.groups.clone() {
prof.groups = groups;
}
+5
View File
@@ -307,6 +307,11 @@ fn main() {
}
set_changelog_toast(Some(update.version.clone()));
let update = if should_restart {
(**update).clone()
} else {
(**update).clone().restart_after_install(false)
};
match update.install(data) {
Ok(()) => {
if should_restart {
+4 -1
View File
@@ -31,7 +31,10 @@
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwMzM5QkE0M0FCOERBMzkKUldRNTJyZzZwSnN6SUdPRGdZREtUUGxMblZqeG9OVHYxRUlRTzJBc2U3MUNJaDMvZDQ1UytZZmYK",
"endpoints": ["https://launcher-files.modrinth.com/updates.json"]
"endpoints": ["https://launcher-files.modrinth.com/updates.json"],
"windows": {
"installMode": "quiet"
}
}
}
}
-1
View File
@@ -40,4 +40,3 @@ These composables are deprecated and should not be used in new code:
- **`useAsyncData`** - we use tanstack, not nuxt's built in async data utility.
- **`useBaseFetch`** (`src/composables/fetch.js`) — legacy Labrinth fetch wrapper. Use `client.labrinth.*` modules instead.
- **`useServersFetch`** (`src/composables/servers/servers-fetch.ts`) — legacy Archon fetch wrapper with manual retry/circuit-breaker. Use `client.archon.*` modules instead — refer to the `packages/api-client/CLAUDE.md` for more information.
+1
View File
@@ -224,6 +224,7 @@ export default defineNuxtConfig({
globalThis.INTERCOM_APP_ID ||
'ykeritl9',
production: isProduction(),
cookieSecure: isProduction(),
buildEnv: process.env.BUILD_ENV,
preview: process.env.PREVIEW === 'true',
featureFlagOverrides: getFeatureFlagOverrides(),
+1 -1
View File
@@ -26,7 +26,7 @@
"@types/semver": "^7.7.1",
"autoprefixer": "^10.4.19",
"glob": "^10.2.7",
"nuxt": "^3.20.2",
"nuxt": "=3.20.2",
"postcss": "^8.4.39",
"prettier-plugin-tailwindcss": "^0.6.5",
"sass": "^1.58.0",
@@ -10,7 +10,7 @@
>
<div
v-if="showLegendTopFade"
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-5 bg-gradient-to-b from-surface-3 to-transparent"
class="z-1 pointer-events-none absolute left-0 right-0 top-0 h-5 bg-gradient-to-b from-surface-3 to-transparent"
/>
</Transition>
@@ -99,7 +99,7 @@
>
<div
v-if="showLegendBottomFade"
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-5 bg-gradient-to-t from-surface-3 to-transparent"
class="z-1 pointer-events-none absolute bottom-0 left-0 right-0 h-5 bg-gradient-to-t from-surface-3 to-transparent"
/>
</Transition>
</div>
@@ -132,7 +132,7 @@
:href="event.announcement_url"
target="_blank"
rel="noopener noreferrer"
class="mt-1.5 inline-flex items-center gap-1 text-sm font-medium text-primary underline !transition-all hover:text-contrast"
class="my-0.5 inline-flex items-center gap-1 text-xs font-medium text-primary underline !transition-all hover:text-contrast"
>
{{ formatMessage(analyticsChartMessages.seeAnnouncement) }}
<ExternalIcon class="size-3.5" aria-hidden="true" />
@@ -18,7 +18,7 @@
({{ durationLabel }})
</span>
</span>
<span v-if="previousRangeLabel" class="min-w-0 truncate text-xs text-primary">
<span v-if="previousRangeLabel" class="min-w-0 space-x-1 truncate text-xs text-primary">
<span class="font-medium">{{ previousRangeLabel }}</span>
<span class="font-normal text-secondary">
{{ formatMessage(analyticsChartMessages.previousPeriodShort) }}
@@ -197,6 +197,7 @@ function getEntryAriaLabel(entry: AnalyticsChartTooltipEntry) {
const ONE_DAY_MS = 24 * 60 * 60 * 1000
const ONE_HOUR_MS = 60 * 60 * 1000
const ONE_MINUTE_MS = 60 * 1000
const DATE_LOCALE = 'en-US'
function formatRangeLabel(
start: Date,
@@ -225,13 +226,13 @@ function formatRangeLabel(
}
if (includeTime) {
const startLabel = new Intl.DateTimeFormat(undefined, startOptions).format(start)
const endLabel = new Intl.DateTimeFormat(undefined, timeOptions).format(end)
const startLabel = new Intl.DateTimeFormat(DATE_LOCALE, startOptions).format(start)
const endLabel = new Intl.DateTimeFormat(DATE_LOCALE, timeOptions).format(end)
const range = `${startLabel}${endLabel}`
if (!showTrailingYear) return range
const yearLabel = new Intl.DateTimeFormat(undefined, { year: 'numeric' }).format(end)
const yearLabel = new Intl.DateTimeFormat(DATE_LOCALE, { year: 'numeric' }).format(end)
return `${range}, ${yearLabel}`
}
@@ -244,13 +245,13 @@ function formatRangeLabel(
endOptions = { day: 'numeric' }
}
const startLabel = new Intl.DateTimeFormat(undefined, startOptions).format(start)
const endLabel = new Intl.DateTimeFormat(undefined, endOptions).format(end)
const startLabel = new Intl.DateTimeFormat(DATE_LOCALE, startOptions).format(start)
const endLabel = new Intl.DateTimeFormat(DATE_LOCALE, endOptions).format(end)
const range = `${startLabel}${endLabel}`
if (!showTrailingYear) return range
const yearLabel = new Intl.DateTimeFormat(undefined, { year: 'numeric' }).format(end)
const yearLabel = new Intl.DateTimeFormat(DATE_LOCALE, { year: 'numeric' }).format(end)
return `${range}, ${yearLabel}`
}
@@ -40,7 +40,6 @@ export function useAnalyticsChartEvents(
placeholderData: [],
refetchOnMount: 'always',
retry: false,
staleTime: 0,
})
const localAnalyticsChartEvents = computed(() => analyticsEvents.value ?? [])
@@ -109,7 +109,7 @@ function getRegionDisplayNames(locale: string): Intl.DisplayNames | null {
function formatCountryCode(countryCode: string, formatMessage: FormatMessage): string {
const normalized = countryCode.trim().toUpperCase()
if (normalized === OTHER_COUNTRY_CODE) {
return formatMessage(analyticsMessages.unknown)
return formatMessage(analyticsMessages.other)
}
if (!REGION_CODE_PATTERN.test(normalized)) {
@@ -146,6 +146,9 @@ export function formatBreakdownLabel(
normalizedLowercaseValue === 'other' ||
normalizedLowercaseValue === 'unknown'
) {
if (selectedBreakdown === 'country') {
return formatMessage(analyticsMessages.other)
}
return formatMessage(analyticsMessages.unknown)
}
if (selectedBreakdown === 'country') {
@@ -753,7 +756,7 @@ export function formatMetricValue(
case 'playtime': {
const hours = value / 3600
return formatMessage(analyticsStatCardMessages.playtimeHours, {
hours: hours.toFixed(1),
hours: Math.abs(hours) < 1 ? hours.toFixed(2) : hours.toFixed(1),
})
}
case 'views':
@@ -770,7 +773,11 @@ function formatSmallAxisNumber(value: number): string {
}
const formattedValue = Math.abs(value) < 1 ? value.toFixed(2) : value.toFixed(1)
return formattedValue.replace(/\.?0+$/, '')
return trimTrailingFractionZeros(formattedValue)
}
function trimTrailingFractionZeros(value: string): string {
return value.replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '')
}
const COMPACT_AXIS_UNITS = [
@@ -814,7 +821,7 @@ function formatCompactAxisValue(value: number): string {
return String(truncatedValue)
}
return roundedValue.toFixed(fractionDigitCount).replace(/\.?0+$/, '')
return trimTrailingFractionZeros(roundedValue.toFixed(fractionDigitCount))
}
export function formatAxisValue(
@@ -224,6 +224,19 @@ export const analyticsGraphTitleMessages = defineMessages({
})
export const analyticsStatCardMessages = defineMessages({
monetizationBannerTitle: {
id: 'analytics.stat.monetization-banner.title',
defaultMessage: 'How does monetization work?',
},
monetizationBannerBody: {
id: 'analytics.stat.monetization-banner.body',
defaultMessage:
'Only views and downloads made through Modrinth are eligible for monetization and must pass fraud-prevention filtering. Modrinth App downloads also require the user to be logged in. Because all projects have a similar ratio of monetized downloads, your revenue would not meaningfully change if all downloads were counted.',
},
monetizationBannerLearnMore: {
id: 'analytics.stat.monetization-banner.learn-more',
defaultMessage: 'Learn more',
},
revenueValue: {
id: 'analytics.stat.revenue-value',
defaultMessage: '${value}',
@@ -66,6 +66,7 @@ export function buildAnalyticsTableColumns({
key: getAnalyticsTableBreakdownColumnKey(breakdown),
label: getAnalyticsTableBreakdownColumnLabel(breakdown, formatMessage),
enableSorting: true,
width: breakdown === 'project' ? '25%' : undefined,
})
}
}
@@ -75,6 +76,7 @@ export function buildAnalyticsTableColumns({
key: 'project',
label: formatAnalyticsBreakdownLabel('project', formatMessage),
enableSorting: true,
width: '25%',
})
}
@@ -40,10 +40,12 @@ export function formatAnalyticsTableCompactPlaytime(
formatMessage: FormatMessage,
): string {
const totalSeconds = Math.max(0, Math.round(value))
const hours = totalSeconds / SECONDS_PER_HOUR
const fractionDigits = hours < 1 ? 2 : 1
return formatMessage(analyticsStatCardMessages.playtimeHours, {
hours: (totalSeconds / SECONDS_PER_HOUR).toLocaleString(undefined, {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
hours: hours.toLocaleString(undefined, {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits,
}),
})
}
@@ -12,6 +12,7 @@ import {
getSliceCount,
} from '../analytics-chart/analytics-chart-utils'
import type { FormatMessage } from '../analytics-messages'
import { analyticsMessages } from '../analytics-messages'
import {
ALL_BREAKDOWN_VALUE,
COMBINED_BREAKDOWN_LABEL_SEPARATOR,
@@ -64,6 +65,8 @@ export function buildAnalyticsTableRows({
const timeRange = fetchRequest.time_range
const sliceCount = getSliceCount(timeRange, timeSlices.length)
const currentTimeSlices =
timeSlices.length > sliceCount ? timeSlices.slice(timeSlices.length - sliceCount) : timeSlices
const includeDate = mode === 'date_breakdown'
const breakdownDisplayValues = new Map<string, string>()
const projectDisplayValues = new Map<string, string>()
@@ -119,9 +122,22 @@ export function buildAnalyticsTableRows({
}
function getCombinedBreakdownDisplay(displays: AnalyticsTableBreakdownDisplayValues) {
const unknownBreakdownLabel = formatMessage(analyticsMessages.unknown)
let hasUnknownBreakdownLabel = false
return selectedBreakdowns
.map((breakdown) => displays[breakdown])
.filter((displayValue): displayValue is string => Boolean(displayValue))
.filter((displayValue) => {
if (displayValue !== unknownBreakdownLabel) {
return true
}
if (hasUnknownBreakdownLabel) {
return false
}
hasUnknownBreakdownLabel = true
return true
})
.join(COMBINED_BREAKDOWN_LABEL_SEPARATOR)
}
@@ -184,7 +200,7 @@ export function buildAnalyticsTableRows({
}
}
timeSlices.forEach((slice, sliceIndex) => {
currentTimeSlices.forEach((slice, sliceIndex) => {
const bucketLabel = includeDate ? getBucketLabel(sliceIndex) : undefined
for (const point of slice) {
@@ -30,9 +30,10 @@ export function getAnalyticsBreakdownValue(
case 'user_agent': {
const downloadSource = normalizeBreakdownValue(
'user_agent' in point ? point.user_agent : undefined,
UNKNOWN_BREAKDOWN_VALUE,
)
return downloadSource === ALL_BREAKDOWN_VALUE
? ALL_BREAKDOWN_VALUE
return downloadSource === UNKNOWN_BREAKDOWN_VALUE
? UNKNOWN_BREAKDOWN_VALUE
: getDownloadSourceLabel(downloadSource, formatMessage)
}
case 'download_reason':
@@ -98,5 +99,12 @@ function normalizeBreakdownValue(
fallback = ALL_BREAKDOWN_VALUE,
): string {
const normalized = value?.trim()
const normalizedLowercase = normalized?.toLowerCase()
if (
fallback === UNKNOWN_BREAKDOWN_VALUE &&
(normalizedLowercase === 'unknown' || normalizedLowercase === 'other')
) {
return fallback
}
return normalized && normalized.length > 0 ? normalized : fallback
}
@@ -227,7 +227,7 @@ import {
normalizeSelectedValues as normalizeSelectedFilterValues,
type ProjectVersionFilterOption,
type ProjectVersionFilterOptionProjectMetadata,
} from './query-filter.ts'
} from './query-filter-utils.ts'
type AnalyticsFilterValueCategory = Exclude<AnalyticsQueryFilterCategory, 'project'>
type GameVersionType = 'release' | 'all'
@@ -52,6 +52,7 @@ const {
selectedCustomTimeframeEndDate,
selectedGroupBy,
queryRefreshTimestamp,
analyticsAllTimeStartDate,
refreshAnalyticsQuery,
} = injectAnalyticsDashboardContext()
@@ -107,6 +108,7 @@ function handleTimeframeDraftChange(selection: TimeFramePickerSelection) {
customStartDate: selection.customStartDate,
customEndDate: selection.customEndDate,
nowTimestamp: queryRefreshTimestamp.value,
allTimeStartDate: analyticsAllTimeStartDate.value,
})
const { start, end } = ensureMinimumTimeRange(range.start, range.end)
const durationMinutes = Math.max(1, Math.floor((end.getTime() - start.getTime()) / 60000))
@@ -434,7 +434,7 @@ import {
getAnalyticsStatsForBreakdown,
getAnalyticsStatsForFilterCategory,
getEnabledAnalyticsStatsForState,
} from './query-filter.ts'
} from './query-filter-utils.ts'
import QueryBuilderFilter from './QueryFilter.vue'
import {
ensureMinimumTimeRange,
@@ -894,12 +894,6 @@ function isRevenueHourlyGroupBy(groupBy: AnalyticsGroupByPreset): boolean {
return groupBy === '1h' || groupBy === '6h'
}
function getAllTimeYearGroupStart(end: Date): Date {
const start = new Date(end)
start.setFullYear(2021)
return start
}
const groupByOptions = computed<ComboboxOption<AnalyticsGroupByPreset>[]>(() => {
const timeframeMinutes = selectedTimeframeDurationMinutes.value
const options = groupByPresetOptions.map((option) => {
@@ -1159,13 +1153,7 @@ function buildMetricFilters(
const fetchRequest = computed<Labrinth.Analytics.v3.FetchRequest>(() => {
const rawRange = selectedTimeRange.value
const rawStart =
selectedTimeframeMode.value === 'preset' &&
selectedTimeframe.value === 'all_time' &&
selectedGroupBy.value === 'year'
? getAllTimeYearGroupStart(rawRange.end)
: rawRange.start
const { start, end } = ensureMinimumTimeRange(rawStart, rawRange.end)
const { start, end } = ensureMinimumTimeRange(rawRange.start, rawRange.end)
const groupByMs = getAnalyticsGroupByPresetMinutes(selectedGroupBy.value) * 60 * 1000
const desiredSlices = Math.max(1, Math.floor((end.getTime() - start.getTime()) / groupByMs))
@@ -84,6 +84,7 @@ function subtractCalendarMonths(date: Date, months: number): Date {
export function getTimeRangeForPreset(
preset: AnalyticsTimeframePreset,
nowTimestamp: number,
allTimeStartDate: Date = new Date(Date.UTC(2023, 0, 1, 0, 0, 0, 0)),
): AnalyticsTimeRange {
const now = getRoundedNow(nowTimestamp)
const end = new Date(now)
@@ -130,7 +131,7 @@ export function getTimeRangeForPreset(
}
case 'all_time':
return {
start: new Date(Date.UTC(2023, 0, 1, 0, 0, 0, 0)),
start: new Date(allTimeStartDate),
end,
}
default:
@@ -193,6 +194,7 @@ export function getAnalyticsTimeRange({
customStartDate,
customEndDate,
nowTimestamp,
allTimeStartDate,
}: {
mode: AnalyticsTimeframeMode
preset: AnalyticsTimeframePreset
@@ -201,6 +203,7 @@ export function getAnalyticsTimeRange({
customStartDate: string
customEndDate: string
nowTimestamp: number
allTimeStartDate?: Date
}): AnalyticsTimeRange {
switch (mode) {
case 'last':
@@ -211,7 +214,7 @@ export function getAnalyticsTimeRange({
return getTimeRangeForCustomDateTimeRange(customStartDate, customEndDate)
case 'preset':
default:
return getTimeRangeForPreset(preset, nowTimestamp)
return getTimeRangeForPreset(preset, nowTimestamp, allTimeStartDate)
}
}
@@ -269,6 +272,7 @@ export function useSelectedAnalyticsTimeRange() {
selectedCustomTimeframeStartDate,
selectedCustomTimeframeEndDate,
queryRefreshTimestamp,
analyticsAllTimeStartDate,
} = injectAnalyticsDashboardContext()
const selectedTimeRange = computed(() =>
@@ -280,6 +284,7 @@ export function useSelectedAnalyticsTimeRange() {
customStartDate: selectedCustomTimeframeStartDate.value,
customEndDate: selectedCustomTimeframeEndDate.value,
nowTimestamp: queryRefreshTimestamp.value,
allTimeStartDate: analyticsAllTimeStartDate.value,
}),
)
@@ -1,21 +1,45 @@
<template>
<div class="grid grid-cols-2 gap-3 lg:grid-cols-4">
<StatCard
v-for="card in statCards"
:key="card.key"
:label="card.label"
:stat-label="card.statLabel"
:vs-prev-period-percent="card.vsPrevPeriodPercent"
:icon="card.icon"
:active="activeStat === card.key"
:disabled="card.disabled"
@click="setActiveStat(card.key)"
/>
<div class="flex w-full flex-col gap-3">
<Admonition
v-if="showMonetizationBanner"
type="info"
:header="formatMessage(analyticsStatCardMessages.monetizationBannerTitle)"
show-actions-underneath
dismissible
@dismiss="dismissMonetizationBanner"
>
<div class="text-primary">
{{ formatMessage(analyticsStatCardMessages.monetizationBannerBody) }}
</div>
<template #actions>
<ButtonStyled color="blue">
<a href="https://modrinth.com/legal/cmp-info" target="_blank" class="w-fit !px-4">
{{ formatMessage(analyticsStatCardMessages.monetizationBannerLearnMore) }}
<RightArrowIcon aria-hidden="true" />
</a>
</ButtonStyled>
</template>
</Admonition>
<div class="grid grid-cols-2 gap-3 lg:grid-cols-4">
<StatCard
v-for="card in statCards"
:key="card.key"
:label="card.label"
:stat-label="card.statLabel"
:vs-prev-period-percent="card.vsPrevPeriodPercent"
:icon="card.icon"
:active="activeStat === card.key"
:disabled="card.disabled"
@click="setActiveStat(card.key)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { useFormatNumber, useVIntl } from '@modrinth/ui'
import { RightArrowIcon } from '@modrinth/assets'
import { Admonition, ButtonStyled, useFormatNumber, useVIntl } from '@modrinth/ui'
import { useLocalStorage } from '@vueuse/core'
import {
type AnalyticsDashboardStat,
@@ -25,10 +49,13 @@ import {
import { analyticsStatCardMessages, formatAnalyticsStatLabel } from '../analytics-messages.ts'
import StatCard from './StatCard.vue'
const MONETIZATION_BANNER_DISMISSED_KEY = 'analytics-monetization-banner-dismissed'
const {
activeStat,
setActiveStat,
currentTotals,
previousTotals,
percentChanges,
hasPreviousPeriodComparison,
selectedBreakdowns,
@@ -36,6 +63,11 @@ const {
} = injectAnalyticsDashboardContext()
const formatNumber = useFormatNumber()
const { formatMessage } = useVIntl()
const monetizationBannerDismissed = useLocalStorage(MONETIZATION_BANNER_DISMISSED_KEY, false)
const showMonetizationBanner = computed(
() => selectedBreakdowns.value.includes('monetization') && !monetizationBannerDismissed.value,
)
const MAX_PREVIOUS_PERIOD_PERCENT_DISPLAY = 1000
const compactNumberFormatter = computed(
() =>
@@ -57,16 +89,79 @@ function formatStatNumber(value: number): string {
function formatPercent(value: number): string {
const rounded = Math.round(value * 10) / 10
if (rounded === 0) {
return '0%'
}
const signPrefix = rounded > 0 ? '+' : ''
return `${signPrefix}${rounded.toFixed(1)}%`
}
function formatPreviousPeriodPercent(value: number): string | null {
function formatSignedStatNumber(value: number): string {
const signPrefix = value > 0 ? '+' : ''
return `${signPrefix}${formatStatNumber(value)}`
}
function formatSignedRevenue(value: number): string {
const signPrefix = value > 0 ? '+' : value < 0 ? '-' : ''
return `${signPrefix}${formatMessage(analyticsStatCardMessages.revenueValue, {
value: formatStatNumber(Math.abs(value)),
})}`
}
function formatSignedPlaytimeHours(value: number): string {
const rounded = Math.round(value * 10) / 10
if (rounded === 0) {
return '0'
}
if (Math.abs(rounded) >= 1000) {
const signPrefix = rounded > 0 ? '+' : ''
return `${signPrefix}${compactNumberFormatter.value.format(rounded)}`
}
const signPrefix = rounded > 0 ? '+' : ''
return `${signPrefix}${rounded.toFixed(1)}`
}
function formatSignedPlaytime(value: number): string {
return formatMessage(analyticsStatCardMessages.playtimeHours, {
hours: formatSignedPlaytimeHours(value / 3600),
})
}
function formatPreviousPeriodComparison(
stat: AnalyticsDashboardStat,
percentChange: number,
currentValue: number,
previousValue: number,
): string | null {
if (!hasPreviousPeriodComparison.value) {
return null
}
return formatPercent(value)
const delta = currentValue - previousValue
if (previousValue === 0 && currentValue === 0) {
return formatPercent(percentChange)
}
if (previousValue !== 0 && Math.abs(percentChange) <= MAX_PREVIOUS_PERIOD_PERCENT_DISPLAY) {
return formatPercent(percentChange)
}
switch (stat) {
case 'revenue':
return formatSignedRevenue(delta)
case 'playtime':
return formatSignedPlaytime(delta)
case 'views':
case 'downloads':
return formatSignedStatNumber(delta)
}
}
function dismissMonetizationBanner() {
monetizationBannerDismissed.value = true
}
const statCards = computed<
@@ -83,7 +178,12 @@ const statCards = computed<
key: 'views',
label: formatAnalyticsStatLabel('views', formatMessage),
statLabel: formatStatNumber(currentTotals.value.views),
vsPrevPeriodPercent: formatPreviousPeriodPercent(percentChanges.value.views),
vsPrevPeriodPercent: formatPreviousPeriodComparison(
'views',
percentChanges.value.views,
currentTotals.value.views,
previousTotals.value.views,
),
icon: 'eye',
disabled: !isAnalyticsDashboardStatRelevant('views', selectedBreakdowns.value),
},
@@ -91,7 +191,12 @@ const statCards = computed<
key: 'downloads',
label: formatAnalyticsStatLabel('downloads', formatMessage),
statLabel: formatStatNumber(currentTotals.value.downloads),
vsPrevPeriodPercent: formatPreviousPeriodPercent(percentChanges.value.downloads),
vsPrevPeriodPercent: formatPreviousPeriodComparison(
'downloads',
percentChanges.value.downloads,
currentTotals.value.downloads,
previousTotals.value.downloads,
),
icon: 'download',
disabled: !isAnalyticsDashboardStatRelevant('downloads', selectedBreakdowns.value),
},
@@ -101,7 +206,12 @@ const statCards = computed<
statLabel: formatMessage(analyticsStatCardMessages.revenueValue, {
value: formatStatNumber(currentTotals.value.revenue),
}),
vsPrevPeriodPercent: formatPreviousPeriodPercent(percentChanges.value.revenue),
vsPrevPeriodPercent: formatPreviousPeriodComparison(
'revenue',
percentChanges.value.revenue,
currentTotals.value.revenue,
previousTotals.value.revenue,
),
icon: 'dollar',
disabled: !isAnalyticsDashboardStatRelevant('revenue', selectedBreakdowns.value),
},
@@ -111,7 +221,12 @@ const statCards = computed<
statLabel: formatMessage(analyticsStatCardMessages.playtimeHours, {
hours: formatStatNumber(currentTotals.value.playtime / 3600),
}),
vsPrevPeriodPercent: formatPreviousPeriodPercent(percentChanges.value.playtime),
vsPrevPeriodPercent: formatPreviousPeriodComparison(
'playtime',
percentChanges.value.playtime,
currentTotals.value.playtime,
previousTotals.value.playtime,
),
icon: 'clock',
disabled: !isAnalyticsDashboardStatRelevant('playtime', selectedBreakdowns.value),
},
@@ -1,244 +1,284 @@
<template>
<div
class="notification"
:class="{
'has-body': hasBody,
compact: compact,
read: notification.read,
}"
:class="
type === 'server_invite'
? { read: notification.read }
: {
notification: true,
'has-body': hasBody,
compact: compact,
read: notification.read,
}
"
>
<nuxt-link
v-if="!type"
:to="notification.link"
class="notification__icon backed-svg"
:class="{ raised: raised }"
>
<BellIcon />
</nuxt-link>
<DoubleIcon v-else class="notification__icon">
<template #primary>
<nuxt-link v-if="project" :to="getProjectLink(project)" tabindex="-1">
<Avatar size="xs" :src="project.icon_url" :raised="raised" no-shadow />
</nuxt-link>
<nuxt-link
v-else-if="organization"
:to="`/organization/${organization.slug}`"
tabindex="-1"
>
<Avatar size="xs" :src="organization.icon_url" :raised="raised" no-shadow />
</nuxt-link>
<nuxt-link v-else-if="user" :to="getUserLink(user)" tabindex="-1">
<Avatar size="xs" :src="user.avatar_url" :raised="raised" no-shadow />
</nuxt-link>
<Avatar v-else size="xs" :raised="raised" no-shadow />
</template>
<template #secondary>
<ScaleIcon
v-if="type === 'moderator_message' || type === 'status_change'"
class="moderation-color"
/>
<UserPlusIcon v-else-if="type === 'team_invite' && project" class="creator-color" />
<UserPlusIcon
v-else-if="type === 'organization_invite' && organization"
class="creator-color"
/>
<VersionIcon v-else-if="type === 'project_update' && project && version" />
<BellIcon v-else />
</template>
</DoubleIcon>
<div class="notification__title">
<template v-if="type === 'project_update' && project && version">
A project you follow,
<nuxt-link :to="getProjectLink(project)" class="title-link">{{ project.title }}</nuxt-link>
, has been updated:
</template>
<template v-else-if="type === 'team_invite' && project">
<nuxt-link
:to="`/user/${invitedBy.username}`"
class="iconified-link title-link inline-flex"
>
<Avatar
:src="invitedBy.avatar_url"
circle
size="xxs"
no-shadow
:raised="raised"
class="inline-flex"
/>
<span class="space">&nbsp;</span>
<span>{{ invitedBy.username }}</span>
</nuxt-link>
<span>
has invited you to join
<nuxt-link :to="getProjectLink(project)" class="title-link">
{{ project.title }} </nuxt-link
>.
</span>
</template>
<template v-else-if="type === 'organization_invite' && organization">
<nuxt-link
:to="`/user/${invitedBy.username}`"
class="iconified-link title-link inline-flex"
>
<Avatar
:src="invitedBy.avatar_url"
circle
size="xxs"
no-shadow
:raised="raised"
class="inline-flex"
/>
<span class="space">&nbsp;</span>
<span>{{ invitedBy.username }}</span>
</nuxt-link>
<span>
has invited you to join
<nuxt-link :to="`/organization/${organization.slug}`" class="title-link">
{{ organization.name }} </nuxt-link
>.
</span>
</template>
<template v-else-if="type === 'status_change' && project">
<nuxt-link :to="getProjectLink(project)" class="title-link">
{{ project.title }}
</nuxt-link>
<template v-if="tags.rejectedStatuses.includes(notification.body.new_status)">
has been
<ProjectStatusBadge :status="notification.body.new_status" />
</template>
<template v-else>
updated from
<ProjectStatusBadge :status="notification.body.old_status" />
to
<ProjectStatusBadge :status="notification.body.new_status" />
</template>
by the moderators.
</template>
<template v-else-if="type === 'moderator_message' && thread && project && !report">
Your project,
<nuxt-link :to="getProjectLink(project)" class="title-link">{{ project.title }}</nuxt-link>
, has received
<template v-if="notification.grouped_notifs"> messages</template>
<template v-else>a message</template>
from the moderators.
</template>
<template v-else-if="type === 'moderator_message' && thread && report">
A moderator replied to your report of
<template v-if="version">
version
<nuxt-link :to="getVersionLink(project, version)" class="title-link">
{{ version.name }}
</nuxt-link>
of project
</template>
<nuxt-link v-if="project" :to="getProjectLink(project)" class="title-link">
{{ project.title }}
</nuxt-link>
<nuxt-link v-else-if="user" :to="getUserLink(user)" class="title-link">
{{ user.username }}
</nuxt-link>
.
</template>
<nuxt-link v-else :to="notification.link" class="title-link">
<span v-html="renderString(notification.title)" />
</nuxt-link>
<!-- <span v-else class="known-errors">Error reading notification.</span>-->
</div>
<div v-if="hasBody" class="notification__body">
<ThreadSummary
v-if="type === 'moderator_message' && thread"
:thread="thread"
:link="threadLink"
:raised="raised"
:messages="getMessages()"
class="thread-summary"
:auth="auth"
/>
<div v-else-if="type === 'project_update'" class="version-list">
<template v-if="type === 'server_invite'">
<div class="flex flex-col gap-4">
<ModrinthServersIcon class="h-auto w-56 max-w-full text-[var(--color-heading)]" />
<div
v-for="notif in (notification.grouped_notifs
? [notification, ...notification.grouped_notifs]
: [notification]
).filter((x) => x.extra_data.version)"
:key="notif.id"
class="version-link"
class="flex flex-wrap items-center gap-x-1.5 gap-y-2 text-lg leading-tight text-[var(--color-heading)]"
>
<VersionIcon />
<nuxt-link
:to="getVersionLink(notif.extra_data.project, notif.extra_data.version)"
class="text-link"
v-if="invitedBy"
:to="`/user/${invitedBy.username}`"
class="inline-flex items-center font-bold text-[var(--color-heading)] hover:underline"
>
{{ notif.extra_data.version.name }}
</nuxt-link>
<span class="version-info">
for
<Categories
:categories="getLoaderCategories(notif.extra_data.version)"
:type="notif.extra_data.project.project_type"
class="categories"
<Avatar
:src="invitedBy.avatar_url"
circle
size="xxs"
no-shadow
:raised="raised"
class="mr-1.5 inline-flex"
/>
{{ $formatVersion(notif.extra_data.version.game_versions) }}
<span v-tooltip="formatDateTime(notif.extra_data.version.date_published)" class="date">
{{ formatRelativeTime(notif.extra_data.version.date_published) }}
</span>
</span>
<span>{{ invitedBy.username }}</span>
</nuxt-link>
<span v-if="invitedBy">has invited you to manage</span>
<span v-else>You have been invited to manage</span>
<span
><strong class="font-bold text-[var(--color-heading)]">{{
notification.body.server_name
}}</strong
>.</span
>
</div>
</div>
<template v-else>
{{ notification.text }}
</template>
</div>
<span class="notification__date">
<span v-if="notification.read" class="read-badge inline-flex">
<CheckCircleIcon /> Read
</span>
<span v-tooltip="formatDateTime(notification.created)" class="inline-flex">
<CalendarIcon class="mr-1" /> Received
{{ formatRelativeTime(notification.created) }}
</span>
</span>
<div v-if="compact" class="notification__actions">
<template v-if="type === 'team_invite' || type === 'organization_invite'">
<ButtonStyled circular color="brand" type="transparent">
<button
v-tooltip="`Accept`"
@click="
() => {
acceptTeamInvite(notification.body.team_id)
read()
}
"
>
<CheckIcon />
</button>
</ButtonStyled>
<ButtonStyled circular color="red" type="transparent">
<button
v-tooltip="`Decline`"
@click="
() => {
removeSelfFromTeam(notification.body.team_id)
read()
}
"
>
<XIcon />
</button>
</ButtonStyled>
</template>
<ButtonStyled v-else-if="!notification.read" circular type="transparent">
<button v-tooltip="`Mark as read`" @click="read()">
<XIcon />
</button>
</ButtonStyled>
</div>
<div v-else class="notification__actions">
<div v-if="type !== null" class="input-group">
<template
v-if="(type === 'team_invite' || type === 'organization_invite') && !notification.read"
<div
v-if="!notification.read"
class="flex flex-wrap items-center gap-3"
:class="{ 'gap-2': compact }"
>
<ButtonStyled color="brand">
<button @click="performActionByTitle(notification, 'Accept')">
<CheckIcon />
Accept
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="performActionByTitle(notification, 'Deny')">
<XIcon />
Decline
</button>
</ButtonStyled>
</div>
<div
class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[var(--color-text-secondary)]"
>
<span
v-if="notification.read"
class="inline-flex items-center font-bold text-[var(--color-text)]"
>
<CheckCircleIcon /> Read
</span>
<span v-tooltip="formatDateTime(notification.created)" class="inline-flex items-center">
<CalendarIcon class="mr-1" /> Received
{{ formatRelativeTime(notification.created) }}
</span>
<CopyCode v-if="flags.developerMode" :text="notification.id" />
</div>
</div>
</template>
<template v-else>
<nuxt-link
v-if="!type"
:to="notification.link"
class="notification__icon backed-svg"
:class="{ raised: raised }"
>
<BellIcon />
</nuxt-link>
<DoubleIcon v-else class="notification__icon">
<template #primary>
<nuxt-link v-if="project" :to="getProjectLink(project)" tabindex="-1">
<Avatar size="xs" :src="project.icon_url" :raised="raised" no-shadow />
</nuxt-link>
<nuxt-link
v-else-if="organization"
:to="`/organization/${organization.slug}`"
tabindex="-1"
>
<Avatar size="xs" :src="organization.icon_url" :raised="raised" no-shadow />
</nuxt-link>
<nuxt-link v-else-if="user" :to="getUserLink(user)" tabindex="-1">
<Avatar size="xs" :src="user.avatar_url" :raised="raised" no-shadow />
</nuxt-link>
<Avatar v-else size="xs" :raised="raised" no-shadow />
</template>
<template #secondary>
<ScaleIcon
v-if="type === 'moderator_message' || type === 'status_change'"
class="moderation-color"
/>
<UserPlusIcon v-else-if="type === 'team_invite' && project" class="creator-color" />
<UserPlusIcon
v-else-if="type === 'organization_invite' && organization"
class="creator-color"
/>
<VersionIcon v-else-if="type === 'project_update' && project && version" />
<BellIcon v-else />
</template>
</DoubleIcon>
<div class="notification__title">
<template v-if="type === 'project_update' && project && version">
A project you follow,
<nuxt-link :to="getProjectLink(project)" class="title-link">{{
project.title
}}</nuxt-link>
, has been updated:
</template>
<template v-else-if="type === 'team_invite' && project">
<nuxt-link
:to="`/user/${invitedBy.username}`"
class="iconified-link title-link inline-flex"
>
<Avatar
:src="invitedBy.avatar_url"
circle
size="xxs"
no-shadow
:raised="raised"
class="inline-flex"
/>
<span class="space">&nbsp;</span>
<span>{{ invitedBy.username }}</span>
</nuxt-link>
<span>
has invited you to join
<nuxt-link :to="getProjectLink(project)" class="title-link">
{{ project.title }} </nuxt-link
>.
</span>
</template>
<template v-else-if="type === 'organization_invite' && organization">
<nuxt-link
:to="`/user/${invitedBy.username}`"
class="iconified-link title-link inline-flex"
>
<Avatar
:src="invitedBy.avatar_url"
circle
size="xxs"
no-shadow
:raised="raised"
class="inline-flex"
/>
<span class="space">&nbsp;</span>
<span>{{ invitedBy.username }}</span>
</nuxt-link>
<span>
has invited you to join
<nuxt-link :to="`/organization/${organization.slug}`" class="title-link">
{{ organization.name }} </nuxt-link
>.
</span>
</template>
<template v-else-if="type === 'status_change' && project">
<nuxt-link :to="getProjectLink(project)" class="title-link">
{{ project.title }}
</nuxt-link>
<template v-if="tags.rejectedStatuses.includes(notification.body.new_status)">
has been
<ProjectStatusBadge :status="notification.body.new_status" />
</template>
<template v-else>
updated from
<ProjectStatusBadge :status="notification.body.old_status" />
to
<ProjectStatusBadge :status="notification.body.new_status" />
</template>
by the moderators.
</template>
<template v-else-if="type === 'moderator_message' && thread && project && !report">
Your project,
<nuxt-link :to="getProjectLink(project)" class="title-link">{{
project.title
}}</nuxt-link>
, has received
<template v-if="notification.grouped_notifs"> messages</template>
<template v-else>a message</template>
from the moderators.
</template>
<template v-else-if="type === 'moderator_message' && thread && report">
A moderator replied to your report of
<template v-if="version">
version
<nuxt-link :to="getVersionLink(project, version)" class="title-link">
{{ version.name }}
</nuxt-link>
of project
</template>
<nuxt-link v-if="project" :to="getProjectLink(project)" class="title-link">
{{ project.title }}
</nuxt-link>
<nuxt-link v-else-if="user" :to="getUserLink(user)" class="title-link">
{{ user.username }}
</nuxt-link>
.
</template>
<nuxt-link v-else :to="notification.link" class="title-link">
<span v-html="renderString(notification.title)" />
</nuxt-link>
<!-- <span v-else class="known-errors">Error reading notification.</span>-->
</div>
<div v-if="hasBody" class="notification__body">
<ThreadSummary
v-if="type === 'moderator_message' && thread"
:thread="thread"
:link="threadLink"
:raised="raised"
:messages="getMessages()"
class="thread-summary"
:auth="auth"
/>
<div v-else-if="type === 'project_update'" class="version-list">
<div
v-for="notif in (notification.grouped_notifs
? [notification, ...notification.grouped_notifs]
: [notification]
).filter((x) => x.extra_data.version)"
:key="notif.id"
class="version-link"
>
<VersionIcon />
<nuxt-link
:to="getVersionLink(notif.extra_data.project, notif.extra_data.version)"
class="text-link"
>
{{ notif.extra_data.version.name }}
</nuxt-link>
<span class="version-info">
for
<Categories
:categories="getLoaderCategories(notif.extra_data.version)"
:type="notif.extra_data.project.project_type"
class="categories"
/>
{{ $formatVersion(notif.extra_data.version.game_versions) }}
<span
v-tooltip="formatDateTime(notif.extra_data.version.date_published)"
class="date"
>
{{ formatRelativeTime(notif.extra_data.version.date_published) }}
</span>
</span>
</div>
</div>
<template v-else>
{{ notification.text }}
</template>
</div>
<span class="notification__date">
<span v-if="notification.read" class="read-badge inline-flex">
<CheckCircleIcon /> Read
</span>
<span v-tooltip="formatDateTime(notification.created)" class="inline-flex">
<CalendarIcon class="mr-1" /> Received
{{ formatRelativeTime(notification.created) }}
</span>
</span>
<div v-if="compact" class="notification__actions">
<template v-if="type === 'team_invite' || type === 'organization_invite'">
<ButtonStyled circular color="brand" type="transparent">
<button
v-tooltip="`Accept`"
@click="
() => {
acceptTeamInvite(notification.body.team_id)
@@ -247,11 +287,11 @@
"
>
<CheckIcon />
Accept
</button>
</ButtonStyled>
<ButtonStyled color="red">
<ButtonStyled circular color="red" type="transparent">
<button
v-tooltip="`Decline`"
@click="
() => {
removeSelfFromTeam(notification.body.team_id)
@@ -260,41 +300,79 @@
"
>
<XIcon />
Decline
</button>
</ButtonStyled>
</template>
<ButtonStyled v-else-if="!notification.read">
<button @click="read()">
<CheckIcon />
Mark as read
<ButtonStyled v-else-if="!notification.read" circular type="transparent">
<button v-tooltip="`Mark as read`" @click="read()">
<XIcon />
</button>
</ButtonStyled>
<CopyCode v-if="flags.developerMode" :text="notification.id" />
</div>
<div v-else class="input-group">
<ButtonStyled v-if="notification.link && notification.link !== '#'">
<nuxt-link :to="notification.link" target="_blank">
<ExternalIcon />
Open link
</nuxt-link>
</ButtonStyled>
<ButtonStyled v-for="(action, actionIndex) in notification.actions" :key="actionIndex">
<button @click="performAction(notification, actionIndex)">
<CheckIcon v-if="action.title === 'Accept'" />
<XIcon v-else-if="action.title === 'Deny'" />
{{ action.title }}
</button>
</ButtonStyled>
<ButtonStyled v-if="notification.actions.length === 0 && !notification.read">
<button @click="performAction(notification, null)">
<CheckIcon />
Mark as read
</button>
</ButtonStyled>
<CopyCode v-if="flags.developerMode" :text="notification.id" />
<div v-else class="notification__actions">
<div v-if="type !== null" class="input-group">
<template
v-if="(type === 'team_invite' || type === 'organization_invite') && !notification.read"
>
<ButtonStyled color="brand">
<button
@click="
() => {
acceptTeamInvite(notification.body.team_id)
read()
}
"
>
<CheckIcon />
Accept
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button
@click="
() => {
removeSelfFromTeam(notification.body.team_id)
read()
}
"
>
<XIcon />
Decline
</button>
</ButtonStyled>
</template>
<ButtonStyled v-else-if="!notification.read">
<button @click="read()">
<CheckIcon />
Mark as read
</button>
</ButtonStyled>
<CopyCode v-if="flags.developerMode" :text="notification.id" />
</div>
<div v-else class="input-group">
<ButtonStyled v-if="notification.link && notification.link !== '#'">
<nuxt-link :to="notification.link" target="_blank">
<ExternalIcon />
Open link
</nuxt-link>
</ButtonStyled>
<ButtonStyled v-for="(action, actionIndex) in notification.actions" :key="actionIndex">
<button @click="performAction(notification, actionIndex)">
<CheckIcon v-if="action.title === 'Accept'" />
<XIcon v-else-if="action.title === 'Deny'" />
{{ action.title }}
</button>
</ButtonStyled>
<ButtonStyled v-if="notification.actions.length === 0 && !notification.read">
<button @click="performAction(notification, null)">
<CheckIcon />
Mark as read
</button>
</ButtonStyled>
<CopyCode v-if="flags.developerMode" :text="notification.id" />
</div>
</div>
</div>
</template>
</div>
</template>
@@ -328,11 +406,13 @@ import { markAsRead } from '~/helpers/platform-notifications'
import { getProjectLink, getVersionLink } from '~/helpers/projects'
import { acceptTeamInvite, removeSelfFromTeam } from '~/helpers/teams'
import ModrinthServersIcon from '../brand/ModrinthServersIcon.vue'
import ThreadSummary from './thread/ThreadSummary.vue'
const client = injectModrinthClient()
const { addNotification } = injectNotificationManager()
const emit = defineEmits(['update:notifications'])
const router = useRouter()
const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
@@ -415,9 +495,29 @@ async function performAction(notification, actionIndex) {
await read()
if (actionIndex !== null) {
await useBaseFetch(`${notification.actions[actionIndex].action_route[1]}`, {
method: notification.actions[actionIndex].action_route[0].toUpperCase(),
})
const action = notification.actions[actionIndex]
if (type.value === 'server_invite') {
const actionName = action.title.toLowerCase()
const inviteAction = actionName === 'accept' ? 'accept' : 'decline'
const serverId = notification.body.server_id
await client.request(`/servers/${serverId}/invites/${inviteAction}`, {
api: 'archon',
version: 1,
method: 'POST',
})
if (inviteAction === 'accept') {
await router.push(`/hosting/manage/${encodeURIComponent(serverId)}`)
}
} else {
const [method, route] = action.action_route
await useBaseFetch(route, {
method: method.toUpperCase(),
})
}
}
} catch (err) {
addNotification({
@@ -429,6 +529,20 @@ async function performAction(notification, actionIndex) {
stopLoading()
}
function performActionByTitle(notification, title) {
const actionIndex = notification.actions.findIndex((action) => action.title === title)
if (actionIndex === -1) {
addNotification({
title: 'An error occurred',
text: `Missing ${title.toLowerCase()} action for notification.`,
type: 'error',
})
return
}
return performAction(notification, actionIndex)
}
function getMessages() {
const messages = []
if (props.notification.body.message_id) {
@@ -1,20 +1,22 @@
<script setup lang="ts">
import type { Archon } from '@modrinth/api-client'
import { PlusIcon, XIcon } from '@modrinth/assets'
import {
Accordion,
ButtonStyled,
injectModrinthClient,
injectNotificationManager,
NewModal,
ServerNotice,
StyledInput,
TagItem,
} from '@modrinth/ui'
import type { ServerNotice as ServerNoticeType } from '@modrinth/utils'
import { ref } from 'vue'
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()
type ServerNoticeType = Archon.Notices.v0.ListedNotice
const modal = ref<InstanceType<typeof NewModal>>()
@@ -32,28 +34,23 @@ const assignedNodes = computed(() => assigned.value.filter((n) => n.kind === 'no
const inputField = ref('')
async function refresh() {
await useServersFetch('notices').then((res) => {
const notices = res as ServerNoticeType[]
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? []
})
const notices = await client.archon.notices_v0.list()
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? []
}
async function assign(server: boolean = true) {
const input = inputField.value.trim()
if (input !== '' && notice.value) {
await useServersFetch(
`notices/${notice.value.id}/assign?${server ? 'server' : 'node'}=${input}`,
{
method: 'PUT',
},
).catch((err) => {
addNotification({
title: 'Error assigning notice',
text: err,
type: 'error',
await client.archon.notices_v0
.assign(notice.value.id, server ? { server: input } : { node: input })
.catch((err) => {
addNotification({
title: 'Error assigning notice',
text: err,
type: 'error',
})
})
})
} else {
addNotification({
title: 'Error assigning notice',
@@ -84,18 +81,15 @@ async function unassignDetect() {
async function unassign(id: string, server: boolean = true) {
if (notice.value) {
await useServersFetch(
`notices/${notice.value.id}/unassign?${server ? 'server' : 'node'}=${id}`,
{
method: 'PUT',
},
).catch((err) => {
addNotification({
title: 'Error unassigning notice',
text: err,
type: 'error',
await client.archon.notices_v0
.unassign(notice.value.id, server ? { server: id } : { node: id })
.catch((err) => {
addNotification({
title: 'Error unassigning notice',
text: err,
type: 'error',
})
})
})
}
await refresh()
}
@@ -125,7 +119,7 @@ defineExpose({ show, hide })
:level="notice.level"
:message="notice.message"
:dismissable="notice.dismissable"
:title="notice.title"
:title="notice.title ?? undefined"
preview
/>
<div class="flex flex-col gap-2">
@@ -133,6 +133,7 @@ import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
Combobox,
injectModrinthClient,
injectNotificationManager,
NewModal,
StyledInput,
@@ -143,9 +144,9 @@ import { DEFAULT_CREDIT_EMAIL_MESSAGE } from '@modrinth/utils/utils.ts'
import { computed, ref } from 'vue'
import { useBaseFetch } from '#imports'
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()
const modal = ref<InstanceType<typeof NewModal>>()
@@ -205,12 +206,12 @@ const applyDisabled = computed(() => {
async function ensureOverview() {
if (regions.value.length || nodeHostnames.value.length) return
try {
const data = await useServersFetch<any>('/nodes/overview', { version: 'internal' })
regions.value = (data.regions || []).map((r: any) => ({
const data = await client.archon.nodes_internal.overview()
regions.value = data.regions.map((r) => ({
value: r.key,
label: `${r.display_name} (${r.key})`,
}))
nodeHostnames.value = data.node_hostnames || []
nodeHostnames.value = data.node_hostnames
if (!selectedRegion.value && regions.value.length) selectedRegion.value = regions.value[0].value
} catch (err) {
addNotification({ title: 'Failed to load nodes overview', text: String(err), type: 'error' })
@@ -198,6 +198,7 @@ import {
ButtonStyled,
Chips,
Combobox,
injectModrinthClient,
injectNotificationManager,
NewModal,
StyledInput,
@@ -207,13 +208,12 @@ import {
import dayjs from 'dayjs'
import { computed, ref } from 'vue'
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
const emit = defineEmits<{
success: []
}>()
const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()
const modal = ref<InstanceType<typeof NewModal>>()
@@ -341,12 +341,12 @@ const submitDisabled = computed(() => {
async function ensureOverview() {
if (regions.value.length || nodeHostnames.value.length) return
try {
const data = await useServersFetch<any>('/nodes/overview', { version: 'internal' })
regions.value = (data.regions || []).map((r: any) => ({
const data = await client.archon.nodes_internal.overview()
regions.value = data.regions.map((r) => ({
value: r.key,
label: `${r.display_name} (${r.key})`,
}))
nodeHostnames.value = data.node_hostnames || []
nodeHostnames.value = data.node_hostnames
if (!selectedRegion.value && regions.value.length) {
selectedRegion.value = regions.value[0].value
}
@@ -364,30 +364,22 @@ async function submit() {
scheduleOption.value === 'now' ? undefined : dayjs(scheduledDate.value).toISOString()
if (mode.value === 'servers') {
await useServersFetch('/transfers/schedule/servers', {
version: 'internal',
method: 'POST',
body: {
server_ids: parsedServerIds.value,
scheduled_at: scheduledAt,
target_region: selectedRegion.value || undefined,
node_tags: selectedTags.value.length > 0 ? selectedTags.value : undefined,
reason: reason.value.trim(),
},
await client.archon.transfers_internal.scheduleServers({
server_ids: parsedServerIds.value,
scheduled_at: scheduledAt,
target_region: selectedRegion.value || undefined,
node_tags: selectedTags.value.length > 0 ? selectedTags.value : undefined,
reason: reason.value.trim(),
})
} else {
await useServersFetch('/transfers/schedule/nodes', {
version: 'internal',
method: 'POST',
body: {
node_hostnames: selectedNodes.value.slice(),
scheduled_at: scheduledAt,
target_region: selectedRegion.value || undefined,
node_tags: selectedTags.value.length > 0 ? selectedTags.value : undefined,
reason: reason.value.trim(),
cordon_nodes: cordonNodes.value,
tag_nodes: tagNodes.value.trim() || undefined,
},
await client.archon.transfers_internal.scheduleNodes({
node_hostnames: selectedNodes.value.slice(),
scheduled_at: scheduledAt,
target_region: selectedRegion.value || undefined,
node_tags: selectedTags.value.length > 0 ? selectedTags.value : undefined,
reason: reason.value.trim(),
cordon_nodes: cordonNodes.value,
tag_nodes: tagNodes.value.trim() || undefined,
})
}
@@ -48,7 +48,12 @@
>{{ isIncome ? '' : '-' }}{{ formatMoney(transaction.amount) }}</span
>
<template v-if="transaction.type === 'withdrawal' && transaction.status === 'in-transit'">
<Tooltip theme="dismissable-prompt" :triggers="['hover', 'focus']" no-auto-focus>
<Tooltip
theme="dismissable-prompt"
class="inline-flex shrink-0"
:triggers="['hover', 'focus']"
no-auto-focus
>
<span class="my-auto align-middle"
><ButtonStyled circular type="outlined" size="small">
<button class="align-middle" @click="cancelPayout">
@@ -565,7 +565,7 @@ const expandedClasses = reactive<Set<string>>(new Set())
const autoExpandedFileIds = reactive<Set<string>>(new Set())
const showCopyFeedback = reactive<Map<string, boolean>>(new Map())
const highlightedSourceCache = reactive<Map<string, { source: string; lines: string[] }>>(new Map())
const LAZY_LOAD_CLASS_SOURCE_MINIMUM = 10
const LAZY_LOAD_CLASS_SOURCE_MINIMUM = 2
interface ClassGroup {
key: string
@@ -152,7 +152,7 @@
v-if="sortedMessages.length > 0"
:disabled="!replyBody || isLoading"
@click="
isApproved(project)
isApproved(project) && !isStaff(auth.user)
? openReplyModal()
: runBlockingAction('reply', () => sendReply())
"
@@ -169,7 +169,7 @@
v-else
:disabled="!replyBody || isLoading"
@click="
isApproved(project)
isApproved(project) && !isStaff(auth.user)
? openReplyModal()
: runBlockingAction('send', () => sendReply())
"
+2 -1
View File
@@ -1,8 +1,9 @@
export const useAffiliates = () => {
const config = useRuntimeConfig()
const affiliateCookie = useCookie('mrs_afl', {
maxAge: 60 * 60 * 24 * 7, // 7 days
sameSite: 'lax',
secure: true,
secure: config.public.cookieSecure,
httpOnly: false,
path: '/',
})
+2 -1
View File
@@ -30,10 +30,11 @@ export const initAuth = async (oldToken = null) => {
}
const route = useRoute()
const config = useRuntimeConfig()
const authCookie = useCookie('auth-token', {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
secure: true,
secure: config.public.cookieSecure,
httpOnly: false,
path: '/',
})
+12 -9
View File
@@ -54,6 +54,8 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
showViewProdRouteBanner: false,
showModeratorProjectMemberUi: false,
showModeratorPrivateMessageHighlight: true,
archonApiStaging: false,
showHostingAccessInstanceAuditLog: false,
} as const)
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
@@ -64,19 +66,20 @@ export type AllFeatureFlags = {
export type PartialFeatureFlags = Partial<AllFeatureFlags>
const COOKIE_OPTIONS = {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
secure: true,
httpOnly: false,
path: '/',
} satisfies CookieOptions<PartialFeatureFlags>
const getCookieOptions = () =>
({
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
secure: useRuntimeConfig().public.cookieSecure,
httpOnly: false,
path: '/',
}) satisfies CookieOptions<PartialFeatureFlags>
export const useFeatureFlags = () =>
useState<AllFeatureFlags>('featureFlags', () => {
const config = useRuntimeConfig()
const savedFlags = useCookie<PartialFeatureFlags>('featureFlags', COOKIE_OPTIONS)
const savedFlags = useCookie<PartialFeatureFlags>('featureFlags', getCookieOptions())
if (!savedFlags.value) {
savedFlags.value = {}
@@ -106,6 +109,6 @@ export const useFeatureFlags = () =>
export const saveFeatureFlags = () => {
const flags = useFeatureFlags()
const cookie = useCookie<PartialFeatureFlags>('featureFlags', COOKIE_OPTIONS)
const cookie = useCookie<PartialFeatureFlags>('featureFlags', getCookieOptions())
cookie.value = flags.value
}
@@ -1,264 +0,0 @@
/**
* @deprecated Use `@modrinth/api-client` via `injectModrinthClient()` instead.
* The api-client's archon modules (`client.archon.servers_v0`, etc.) handle auth,
* retry, and circuit breaking automatically. This composable is kept for legacy
* code that hasn't been migrated yet.
*/
import { PANEL_VERSION } from '@modrinth/api-client'
import type { V1ErrorInfo } from '@modrinth/utils'
import { ModrinthServerError, ModrinthServersFetchError } from '@modrinth/utils'
import { $fetch, FetchError } from 'ofetch'
export interface ServersFetchOptions {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
contentType?: string
body?: Record<string, any>
version?: number | 'internal'
override?: {
url?: string
token?: string
}
retry?: number | boolean
bypassAuth?: boolean
}
export async function useServersFetch<T>(
path: string,
options: ServersFetchOptions = {},
module?: string,
errorContext?: string,
): Promise<T> {
const config = useRuntimeConfig()
const auth = await useAuth()
const authToken = auth.value?.token
if (!authToken && !options.bypassAuth) {
const error = new ModrinthServersFetchError(
'[Modrinth Hosting] Cannot fetch without auth',
10000,
)
throw new ModrinthServerError('Missing auth token', 401, error, module, undefined, undefined)
}
const {
method = 'GET',
contentType = 'application/json',
body,
version = 0,
override,
retry = method === 'GET' ? 3 : 0,
} = options
const circuitBreakerKey = `${module || 'default'}_${path}`
const failureCount = useState<number>(`fetch_failures_${circuitBreakerKey}`, () => 0)
const lastFailureTime = useState<number>(`last_failure_${circuitBreakerKey}`, () => 0)
const now = Date.now()
if (failureCount.value >= 3 && now - lastFailureTime.value < 30000) {
const error = new ModrinthServersFetchError(
'[Modrinth Hosting] Circuit breaker open - too many recent failures',
503,
)
throw new ModrinthServerError(
'Service temporarily unavailable',
503,
error,
module,
undefined,
undefined,
)
}
if (now - lastFailureTime.value > 30000) {
failureCount.value = 0
}
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
/\/$/,
'',
)
if (!base) {
const error = new ModrinthServersFetchError(
'[Modrinth Hosting] Cannot fetch without base url. Make sure to set a PYRO_BASE_URL in environment variables',
10001,
)
throw new ModrinthServerError(
'Configuration error: Missing PYRO_BASE_URL',
500,
error,
module,
undefined,
undefined,
)
}
const versionString = `v${version}`
let newOverrideUrl = override?.url
if (newOverrideUrl && newOverrideUrl.includes('v0') && version !== 0) {
newOverrideUrl = newOverrideUrl.replace('v0', versionString)
}
const fullUrl = newOverrideUrl
? `https://${newOverrideUrl}/${path.replace(/^\//, '')}`
: version === 0
? `${base}/modrinth/v${version}/${path.replace(/^\//, '')}`
: version === 'internal'
? `${base}/_internal/${path.replace(/^\//, '')}`
: `${base}/v${version}/${path.replace(/^\//, '')}`
const headers: Record<string, string> = {
'User-Agent': 'Modrinth/1.0 (https://modrinth.com)',
'X-Archon-Request': 'true',
'X-Panel-Version': String(PANEL_VERSION),
Vary: 'Accept, Origin',
}
if (!options.bypassAuth) {
headers.Authorization = `Bearer ${override?.token ?? authToken}`
headers['Access-Control-Allow-Headers'] = 'Authorization'
}
if (contentType !== 'none') {
headers['Content-Type'] = contentType
}
if (import.meta.client && typeof window !== 'undefined') {
headers.Origin = window.location.origin
}
let attempts = 0
const maxAttempts = (typeof retry === 'boolean' ? (retry ? 3 : 1) : retry) + 1
let lastError: Error | null = null
while (attempts < maxAttempts) {
try {
const response = await $fetch<T>(fullUrl, {
method,
headers,
body:
body && contentType === 'application/json' ? JSON.stringify(body) : (body ?? undefined),
timeout: 10000,
})
failureCount.value = 0
return response
} catch (error) {
lastError = error as Error
attempts++
if (error instanceof FetchError) {
const statusCode = error.response?.status
const statusText = error.response?.statusText || 'Unknown error'
if (statusCode && statusCode >= 500) {
failureCount.value++
lastFailureTime.value = now
}
let v1Error: V1ErrorInfo | undefined
if (error.data?.error && error.data?.description) {
v1Error = {
context: errorContext,
...error.data,
}
}
const errorMessages: { [key: number]: string } = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
408: 'Request Timeout',
429: "You're making requests too quickly. Please wait a moment and try again.",
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
}
const message =
statusCode && statusCode in errorMessages
? errorMessages[statusCode]
: `HTTP Error: ${statusCode || 'unknown'} ${statusText}`
const isRetryable = statusCode ? [408, 429].includes(statusCode) : false
const is5xxRetryable =
statusCode && statusCode >= 500 && statusCode < 600 && method === 'GET' && attempts === 1
if (!(isRetryable || is5xxRetryable) || attempts >= maxAttempts) {
console.error('Fetch error:', error)
const fetchError = new ModrinthServersFetchError(
`[Modrinth Hosting] ${error.message}`,
statusCode,
error,
)
throw new ModrinthServerError(
`[Modrinth Hosting] ${message}`,
statusCode,
fetchError,
module,
v1Error,
error.data,
)
}
const baseDelay = statusCode && statusCode >= 500 ? 5000 : 1000
const delay = Math.min(baseDelay * Math.pow(2, attempts - 1) + Math.random() * 1000, 15000)
console.warn(`Retrying request in ${delay}ms (attempt ${attempts}/${maxAttempts - 1})`)
await new Promise((resolve) => setTimeout(resolve, delay))
continue
}
console.error('Unexpected fetch error:', error)
const fetchError = new ModrinthServersFetchError(
'[Modrinth Hosting] An unexpected error occurred during the fetch operation.',
undefined,
error as Error,
)
throw new ModrinthServerError(
'Unexpected error during fetch operation',
undefined,
fetchError,
module,
undefined,
undefined,
)
}
}
console.error('All retry attempts failed:', lastError)
if (lastError instanceof FetchError) {
const statusCode = lastError.response?.status
const pyroError = new ModrinthServersFetchError(
'Maximum retry attempts reached',
statusCode,
lastError,
)
throw new ModrinthServerError(
'Maximum retry attempts reached',
statusCode,
pyroError,
module,
undefined,
lastError.data,
)
}
const fetchError = new ModrinthServersFetchError(
'Maximum retry attempts reached',
undefined,
lastError || undefined,
)
throw new ModrinthServerError(
'Maximum retry attempts reached',
undefined,
fetchError,
module,
undefined,
undefined,
)
}
@@ -17,7 +17,6 @@ export type FilterSelection = {
const cookieDefaults = {
maxAge: TEN_MINUTES,
sameSite: 'lax' as const,
secure: true,
path: '/',
httpOnly: false,
}
@@ -52,13 +51,19 @@ function newFilterSelection(
}
export function useCdnDownloadContext() {
const filterGameVersionCookie = useCookie<string | null>('mr_download_filter_game_version', {
const config = useRuntimeConfig()
const cookieOptions = {
...cookieDefaults,
secure: config.public.cookieSecure,
}
const filterGameVersionCookie = useCookie<string | null>('mr_download_filter_game_version', {
...cookieOptions,
default: () => null,
})
const filterLoaderCookie = useCookie<string | null>('mr_download_filter_loader', {
...cookieDefaults,
...cookieOptions,
default: () => null,
})
+3 -1
View File
@@ -14,6 +14,7 @@ import {
import type { Ref } from 'vue'
import { useFeatureFlags } from '~/composables/featureFlags.ts'
import { withStagingArchonBaseUrl } from '~/helpers/archon.ts'
async function getRateLimitKeyFromSecretsStore(): Promise<string | undefined> {
try {
@@ -37,7 +38,8 @@ export function createModrinthClient(
const clientConfig: NuxtClientConfig = {
labrinthBaseUrl: config.apiBaseUrl,
archonBaseUrl: config.archonBaseUrl,
archonBaseUrl: () =>
withStagingArchonBaseUrl(config.archonBaseUrl, flags.value.archonApiStaging),
archonSentryCapture: () => flags.value.archonSentryCapture,
rateLimitKey: config.rateLimitKey || getRateLimitKeyFromSecretsStore,
features: [
+12
View File
@@ -0,0 +1,12 @@
export const STAGING_ARCHON_BASE_URL = 'https://staging-archon.modrinth.com/'
export function withStagingArchonBaseUrl(
baseUrl: string,
useStaging = useFeatureFlags().value.archonApiStaging,
) {
if (!useStaging) {
return baseUrl
}
return STAGING_ARCHON_BASE_URL
}
@@ -1115,9 +1115,6 @@
"frog.title": {
"message": "ضفدع"
},
"hosting-marketing.available-locations": {
"message": "متاح في أمريكا الشمالية، أوروبا، و جنوب شرق اۤسيا لتغطية أوسع."
},
"hosting-marketing.billing.monthly": {
"message": "ادفع شهريا"
},
@@ -8,6 +8,69 @@
"admin.billing.error.not-found": {
"message": "Uživatel nebyl nalezen"
},
"analytics.action.add": {
"message": "Přidat"
},
"analytics.action.cancel": {
"message": "Zrušit"
},
"analytics.action.refresh": {
"message": "Obnovit"
},
"analytics.action.reset": {
"message": "Obnovit"
},
"analytics.breakdown.country": {
"message": "Země"
},
"analytics.breakdown.download-reason": {
"message": "Důvod stažení"
},
"analytics.breakdown.download-source": {
"message": "Zdroj stažení"
},
"analytics.breakdown.game-version": {
"message": "Verze hry"
},
"analytics.breakdown.generic": {
"message": "Rozpis"
},
"analytics.breakdown.loader": {
"message": "Načítač"
},
"analytics.breakdown.monetization": {
"message": "Monetizace"
},
"analytics.breakdown.project": {
"message": "Projekt"
},
"analytics.breakdown.project-status": {
"message": "Stav projektu"
},
"analytics.breakdown.project-version": {
"message": "Verze projektu"
},
"analytics.breakdown.selected": {
"message": "Rozpis {breakdown}"
},
"analytics.chart.action.show-all": {
"message": "Ukázat vše"
},
"analytics.chart.action.show-limited": {
"message": "Zobrazit limitovaný"
},
"analytics.chart.action.show-top-eight": {
"message": "Zobrazit top 8"
},
"analytics.chart.axis.playtime-hours": {
"message": "{hours} h"
},
"analytics.chart.controls.active-count": {
"message": "{count} aktivní"
},
"analytics.chart.controls.annotations": {
"message": "Anotace"
},
"app-marketing.download.description": {
"message": "Naše aplikace je dostupná pro všechny počítačové platformy, vyberte si požadovanou verzi."
},
@@ -947,9 +947,6 @@
"frog.title": {
"message": "Frø"
},
"hosting-marketing.available-locations": {
"message": "Tilgængelig i Nord Amerika, Europa, og Sydøst Asien for bred dækning."
},
"hosting-marketing.billing.monthly": {
"message": "Betal hvert måned"
},
+267 -90
View File
@@ -24,16 +24,16 @@
"message": "Land"
},
"analytics.breakdown.download-reason": {
"message": "Downloadgrund"
"message": "Download-Grund"
},
"analytics.breakdown.download-source": {
"message": "Downloadquelle"
"message": "Download-Quelle"
},
"analytics.breakdown.game-version": {
"message": "Spielversion"
},
"analytics.breakdown.generic": {
"message": "Auswertung"
"message": "Aufschlüsselung"
},
"analytics.breakdown.loader": {
"message": "Loader"
@@ -42,7 +42,7 @@
"message": "Monetarisierung"
},
"analytics.breakdown.none.selected": {
"message": "Keine Auswertung"
"message": "Keine Aufschlüsselung"
},
"analytics.breakdown.project": {
"message": "Projekt"
@@ -54,13 +54,13 @@
"message": "Projektversion"
},
"analytics.breakdown.selected": {
"message": "Auswertung nach {breakdown}"
"message": "Aufschlüsselung nach {breakdown}"
},
"analytics.chart.action.show-all": {
"message": "Alle anzeigen"
},
"analytics.chart.action.show-limited": {
"message": "Zeige limitierte"
"message": "Begrenzt anzeigen"
},
"analytics.chart.action.show-top-eight": {
"message": "Top 8 anzeigen"
@@ -74,29 +74,41 @@
"analytics.chart.controls.annotations": {
"message": "Anmerkungen"
},
"analytics.chart.controls.aria": {
"message": "Analytics-Diagramm-Steuerung, {activeCount}"
},
"analytics.chart.controls.button": {
"message": "Steuerung"
},
"analytics.chart.controls.dialog-aria": {
"message": "Analyse Graph Kontrolle"
"message": "Analytics-Diagramm-Steuerung"
},
"analytics.chart.controls.display": {
"message": "Anzeige"
},
"analytics.chart.controls.modrinth-events": {
"message": "Modrinth Ereignisse"
"message": "Modrinth-Ereignisse"
},
"analytics.chart.controls.no-modrinth-events": {
"message": "Keine Modrinth Ereignisse im Graph."
"message": "Keine Modrinth-Ereignisse im Diagramm."
},
"analytics.chart.controls.no-project-events": {
"message": "Keine Projektereignisse im Graph."
"message": "Keine Projekt-Ereignisse im Diagramm."
},
"analytics.chart.controls.previous-period": {
"message": "Vorheriger Zeitraum"
},
"analytics.chart.controls.project-events": {
"message": "Projektereignisse"
"message": "Projekt-Ereignisse"
},
"analytics.chart.controls.ratio": {
"message": "Verhältnis"
},
"analytics.chart.empty.select-table-items": {
"message": "Wähle Elemente aus der Tabelle unten, um deine Daten zu visualisieren."
},
"analytics.chart.events.count-aria": {
"message": "{count, plural, one {# Analyseereignis} other {# Analyseereignisse}}"
"message": "{count, plural, one {# Analytics-Ereignis} other {# Analytics-Ereignisse}}"
},
"analytics.chart.events.project-title": {
"message": "<project>{projectName}</project>: {title}"
@@ -105,25 +117,34 @@
"message": "Ankündigung ansehen"
},
"analytics.chart.legend.monetization-details.aria": {
"message": "Zeige monetarisierte Analyse Details"
"message": "Details zur monetarisierten Analytics ansehen"
},
"analytics.chart.legend.monetization-details.description": {
"message": "Nur Aufrufe und Downloads über Modrinth zählen für die Monetarisierung, und für Downloads müssen Benutzer angemeldet sein."
},
"analytics.chart.legend.monetization-details.title": {
"message": "Details zur monetarisierten Analytics"
},
"analytics.chart.legend.previous-period-suffix": {
"message": "{name} (Vorherige)"
"message": "{name} (Vorh.)"
},
"analytics.chart.render-limit.description": {
"message": "Anzeigen aller ausgewählten Zeilen von der Liste könnte die Performance beeinflussen."
"message": "Die Anzeige aller ausgewählten Linien aus der Tabelle kann die Seitenleistung beeinträchtigen."
},
"analytics.chart.render-limit.header": {
"message": "Alle {count} Zeilen im Graph anzeigen?"
"message": "Alle {count} Linien im Diagramm anzeigen?"
},
"analytics.chart.table-selection.all": {
"message": "Zeige alle {itemType, select, project {{count, plural, one {Projekt} other {Projekte}}} country {{count, plural, one {Land} other {Länder}}} monetization {{count, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{count, plural, one {Download-Quelle} other {Download-Quellen}}} downloadReason {{count, plural, one {Download-Grund} other {Download-Gründe}}} projectVersion {{count, plural, one {Projektversion} other {Projektversionen}}} loader {{count, plural, one {Loader} other {Loader}}} gameVersion {{count, plural, one {Spielversion} other {Spielversionen}}} other {{count, plural, one {Element} other {Elemente}}}} aus der Tabelle an"
},
"analytics.chart.table-selection.count": {
"message": "Zeige {count} {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} der Liste"
"message": "Zeige {count} {itemType, select, project {{count, plural, one {Projekt} other {Projekte}}} country {{count, plural, one {Land} other {Länder}}} monetization {{count, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{count, plural, one {Download-Quelle} other {Download-Quellen}}} downloadReason {{count, plural, one {Download-Grund} other {Download-Gründe}}} projectVersion {{count, plural, one {Projektversion} other {Projektversionen}}} loader {{count, plural, one {Loader} other {Loader}}} gameVersion {{count, plural, one {Spielversion} other {Spielversionen}}} other {{count, plural, one {Element} other {Elemente}}}} aus der Tabelle an"
},
"analytics.chart.table-selection.limited": {
"message": "Zeige {limit} {itemType, select, project {{limit, plural, one {project} other {projects}}} country {{limit, plural, one {country} other {countries}}} monetization {{limit, plural, one {monetization value} other {monetization values}}} downloadSource {{limit, plural, one {download source} other {download sources}}} downloadReason {{limit, plural, one {download reason} other {download reasons}}} projectVersion {{limit, plural, one {project version} other {project versions}}} loader {{limit, plural, one {loader} other {loaders}}} gameVersion {{limit, plural, one {game version} other {game versions}}} other {{limit, plural, one {item} other {items}}}} der Liste"
"message": "Zeige {limit} {itemType, select, project {{limit, plural, one {Projekt} other {Projekte}}} country {{limit, plural, one {Land} other {Länder}}} monetization {{limit, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{limit, plural, one {Download-Quelle} other {Download-Quellen}}} downloadReason {{limit, plural, one {Download-Grund} other {Download-Gründe}}} projectVersion {{limit, plural, one {Projektversion} other {Projektversionen}}} loader {{limit, plural, one {Loader} other {Loader}}} gameVersion {{limit, plural, one {Spielversion} other {Spielversionen}}} other {{limit, plural, one {Element} other {Elemente}}}} aus der Tabelle an"
},
"analytics.chart.table-selection.top": {
"message": "Zeige top {count} {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} von der Liste"
"message": "Zeige die Top {count} {itemType, select, project {{count, plural, one {Projekt} other {Projekte}}} country {{count, plural, one {Land} other {Länder}}} monetization {{count, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{count, plural, one {Download-Quelle} other {Download-Quellen}}} downloadReason {{count, plural, one {Download-Grund} other {Download-Gründe}}} projectVersion {{count, plural, one {Projektversion} other {Projektversionen}}} loader {{count, plural, one {Loader} other {Loader}}} gameVersion {{count, plural, one {Spielversion} other {Spielversionen}}} other {{count, plural, one {Element} other {Elemente}}}} aus der Tabelle an"
},
"analytics.chart.tooltip.duration.days": {
"message": "{count, plural, one {# Tag} other {# Tage}}"
@@ -137,14 +158,20 @@
"analytics.chart.tooltip.hide-entry": {
"message": "{name} im Diagramm ausblenden"
},
"analytics.chart.tooltip.pinned": {
"message": "Diagramm-Tooltip angeheftet"
},
"analytics.chart.tooltip.pinned-aria": {
"message": "Angepinnt"
"message": "Angeheftet"
},
"analytics.chart.tooltip.previous-period-short": {
"message": "(Vorherige)"
"message": "(vorh.)"
},
"analytics.chart.tooltip.show-entry": {
"message": "{name} im Diagramm anzeigen"
},
"analytics.chart.tooltip.total": {
"message": "Gesamt"
"message": "Total"
},
"analytics.chart.view.area": {
"message": "Fläche"
@@ -162,10 +189,10 @@
"message": "Modpack"
},
"analytics.download-reason.standalone": {
"message": "Eigenständig"
"message": "Standalone"
},
"analytics.download-reason.update": {
"message": "Aktualisierung"
"message": "Update"
},
"analytics.download-source.app": {
"message": "Modrinth App"
@@ -174,55 +201,55 @@
"message": "Modrinth Website"
},
"analytics.downloads.suffix": {
"message": "Downloads"
"message": "downloads"
},
"analytics.empty.no-data": {
"message": "Keine Daten verfügbar"
},
"analytics.empty.no-data-for-analytics": {
"message": "Keine Daten verfügbar für diese Analysen"
"message": "Keine Daten für Analytics verfügbar"
},
"analytics.empty.no-projects": {
"message": "Keine Projekte verfügbar"
},
"analytics.empty.no-projects-for-analytics": {
"message": "Keine Projekte für die Analyse verfügbar"
"message": "Keine Projekte für Analytics verfügbar"
},
"analytics.empty.select-project": {
"message": "Wähle mindestens ein Projekt aus um Daten anzuzeigen"
"message": "Wähle mindestens ein Projekt aus, um Daten anzuzeigen"
},
"analytics.filter.game-version-type": {
"message": "Spiel Version wählen"
"message": "Spielversionstyp"
},
"analytics.filter.game-version-type.all": {
"message": "Alle"
},
"analytics.filter.game-version-type.release": {
"message": "Veröffentlichung"
"message": "Release"
},
"analytics.filter.search.countries": {
"message": "Länder durchsuchen..."
"message": "Länder suchen..."
},
"analytics.filter.search.download-sources": {
"message": "Download-Quellen durchsuchen..."
"message": "Download-Quellen suchen..."
},
"analytics.filter.search.project-versions": {
"message": "Projektversionen durchsuchen..."
"message": "Projektversionen suchen..."
},
"analytics.filter.search.versions": {
"message": "Versionen durchsuchen..."
"message": "Versionen suchen..."
},
"analytics.graph.title.downloads": {
"message": "Downloads im Laufe der Zeit"
"message": "Downloads im Zeitverlauf"
},
"analytics.graph.title.playtime": {
"message": "Spielzeit im Laufe der Zeit"
"message": "Spielzeit im Zeitverlauf"
},
"analytics.graph.title.revenue": {
"message": "Einnahmen im Laufe der Zeit"
"message": "Einnahmen im Zeitverlauf"
},
"analytics.graph.title.views": {
"message": "Ansichten im Laufe der Zeit"
"message": "Aufrufe im Zeitverlauf"
},
"analytics.group-by.1h": {
"message": "1 Std."
@@ -267,19 +294,19 @@
"message": "Ergebnisse werden abgerufen..."
},
"analytics.options.loading": {
"message": "Wird geladen..."
"message": "Lädt..."
},
"analytics.project-event.project-approved": {
"message": "Projekt angenommen"
"message": "Projekt genehmigt"
},
"analytics.project-event.project-private": {
"message": "Projekt auf Privat gestellt"
"message": "Projekt auf privat gesetzt"
},
"analytics.project-event.project-status-changed": {
"message": "Projektstatus geändert"
},
"analytics.project-event.project-unlisted": {
"message": "Projekt entlistet"
"message": "Projekt nicht gelistet"
},
"analytics.project-event.version-released": {
"message": "{version} veröffentlicht"
@@ -288,7 +315,7 @@
"message": "Version hochgeladen"
},
"analytics.project-status.approved": {
"message": "Angenommen"
"message": "Genehmigt"
},
"analytics.project-status.archived": {
"message": "Archiviert"
@@ -318,7 +345,7 @@
"message": "{count, plural, one {# Projekt} other {# Projekte}}"
},
"analytics.project.icon-alt": {
"message": "{name} Icon"
"message": "{name} Symbol"
},
"analytics.project.select": {
"message": "Projekte auswählen"
@@ -327,7 +354,7 @@
"message": "Filter hinzufügen"
},
"analytics.query.label.breakdown": {
"message": "Auswertung:"
"message": "Aufschlüsselung:"
},
"analytics.query.label.grouped-by": {
"message": "Gruppiert nach"
@@ -336,11 +363,20 @@
"message": "Projekt:"
},
"analytics.query.label.timeframe": {
"message": "Zeitfenster:"
"message": "Zeitraum:"
},
"analytics.stat.downloads": {
"message": "Downloads"
},
"analytics.stat.monetization-banner.body": {
"message": "Nur Aufrufe und Downloads, die über Modrinth erfolgen, sind monetarisierbar und müssen die Filter zur Betrugsprävention bestehen. Für Downloads über die Modrinth App muss der Nutzer zudem angemeldet sein. Da alle Projekte einen ähnlichen Anteil an monetarisierten Downloads aufweisen, würden sich deine Einnahmen nicht wesentlich ändern, wenn alle Downloads gezählt würden."
},
"analytics.stat.monetization-banner.learn-more": {
"message": "Mehr erfahren"
},
"analytics.stat.monetization-banner.title": {
"message": "Wie funktioniert die Monetarisierung?"
},
"analytics.stat.playtime": {
"message": "Spielzeit"
},
@@ -348,10 +384,10 @@
"message": "{hours} Std."
},
"analytics.stat.previous-period-comparison": {
"message": "im Vergleich zum vorherigen Zeitraum"
"message": "vs. vorh. Zeitraum"
},
"analytics.stat.previous-period-comparison-short": {
"message": "im Vergleich zum vorherigen"
"message": "vs. vorh."
},
"analytics.stat.revenue": {
"message": "Einnahmen"
@@ -363,17 +399,23 @@
"message": "N/A"
},
"analytics.stat.unavailable-tooltip": {
"message": "Statistik nicht verfügbar für aktuellen Filter"
"message": "Statistik für aktuelle Abfrage nicht verfügbar"
},
"analytics.stat.views": {
"message": "Ansichten"
"message": "Aufrufe"
},
"analytics.table.csv.date-range": {
"message": "{start} bis {end}"
},
"analytics.table.csv.filename": {
"message": "Modrinth Analytics {breakdown} Aufschlüsselung - {dateRange}"
},
"analytics.table.csv.header.playtime-seconds": {
"message": "Spielzeit (Sekunden)"
},
"analytics.table.csv.selected-range": {
"message": "Ausgewählter Bereich"
},
"analytics.table.duration.days": {
"message": "{count, plural, one {# Tag} other {# Tage}}"
},
@@ -383,6 +425,9 @@
"analytics.table.duration.minutes": {
"message": "{count, plural, one {# Minute} other {# Minuten}}"
},
"analytics.table.empty.no-matching-rows": {
"message": "Keine übereinstimmenden Analytics-Zeilen"
},
"analytics.table.export-csv": {
"message": "CSV exportieren"
},
@@ -393,19 +438,19 @@
"message": "Gruppiert nach {groupBy}"
},
"analytics.table.pagination.summary": {
"message": "{start} bis {end} von {total} werden angezeigt"
"message": "Zeige {start} bis {end} von {total}"
},
"analytics.table.search.placeholder": {
"message": "Suchen..."
"message": "Suche..."
},
"analytics.threshold.countries-above": {
"message": "Länder über"
},
"analytics.threshold.country-downloads-aria": {
"message": "Schwellenwert für Downloads nach Land"
"message": "Schwellenwert für Länder-Downloads"
},
"analytics.threshold.game-version-downloads-aria": {
"message": "Schwellenwert für Downloads von Spielversionen"
"message": "Schwellenwert für Spielversion-Downloads"
},
"analytics.threshold.game-versions-above": {
"message": "Spielversionen über"
@@ -423,7 +468,7 @@
"message": "Projekte über"
},
"analytics.title": {
"message": "Analysen"
"message": "Analytics"
},
"analytics.value.monetized": {
"message": "Monetarisiert"
@@ -843,61 +888,112 @@
"message": "Private Notiz hinzufügen"
},
"conversation-thread.action.approve": {
"message": "Annehmen"
"message": "Genehmigen"
},
"conversation-thread.action.approve-with-reply": {
"message": "Mit Antwort annehmen"
"message": "Genehmigen mit Antwort"
},
"conversation-thread.action.close-thread": {
"message": ""
"message": "Thread schliessen"
},
"conversation-thread.action.close-with-reply": {
"message": "Mit Antwort schließen"
"message": "Schliessen mit Antwort"
},
"conversation-thread.action.reject": {
"message": "Ablehnen"
},
"conversation-thread.action.reject-with-reply": {
"message": "Mit Antwort ablehnen"
"message": "Ablehnen mit Antwort"
},
"conversation-thread.action.reopen-thread": {
"message": "Öffne den Thread wieder"
"message": "Thread wieder öffnen"
},
"conversation-thread.action.reply": {
"message": "Antworten"
},
"conversation-thread.action.reply-to-thread": {
"message": "Antworte dem Thread"
"message": "Auf Thread antworten"
},
"conversation-thread.action.resubmit-for-review": {
"message": "Erneut zur Überprüfung einreichen"
},
"conversation-thread.action.resubmit-for-review-with-reply": {
"message": "Erneut zur Überprüfung einreichen mit Antwort"
},
"conversation-thread.action.send": {
"message": "Senden"
},
"conversation-thread.action.send-to-review": {
"message": "Absenden zur Überprüfung"
"message": "Zur Überprüfung senden"
},
"conversation-thread.action.send-to-review-with-reply": {
"message": "Zur Überprüfung senden mit Antwort"
},
"conversation-thread.action.set-to-draft": {
"message": "Als Entwurf festlegen"
},
"conversation-thread.action.set-to-draft-with-reply": {
"message": "Als Entwurf festlegen mit Antwort"
},
"conversation-thread.action.withhold": {
"message": "Zurückhalten"
},
"conversation-thread.action.withhold-with-reply": {
"message": "Mit Antwort zurückhalten"
"message": "Zurückhalten mit Antwort"
},
"conversation-thread.closed-thread.description": {
"message": "Dieser Thread ist geschlossen und es können keine neuen Nachrichten gesendet werden."
},
"conversation-thread.error.closing-report": {
"message": "Fehler beim Schliessen der Meldung"
},
"conversation-thread.error.reopening-report": {
"message": "Fehler beim erneuten Öffnen der Meldung"
},
"conversation-thread.error.sending-message": {
"message": "Fehler beim Senden der Nachricht"
},
"conversation-thread.reply-editor.placeholder.reply": {
"message": "Auf Thread antworten..."
},
"conversation-thread.reply-editor.placeholder.send": {
"message": "Sende eine Nachricht..."
"message": "Eine Nachricht senden..."
},
"conversation-thread.reply-modal.confirmation.description": {
"message": "Bestätigen, dass Moderatoren dies nicht aktiv überwachen"
},
"conversation-thread.reply-modal.confirmation.label": {
"message": "Mir ist bewusst, dass dieser Thread nicht aktiv von den Moderatoren überwacht wird."
"message": "Ich nehme zur Kenntnis, dass die Moderatoren den Thread nicht aktiv überwachen."
},
"conversation-thread.reply-modal.description": {
"message": "Dein Projekt ist bereits genehmigt. Daher überwacht das Moderationsteam diesen Thread nicht aktiv. Sie können deine Nachricht jedoch trotzdem sehen, falls ein Problem mit deinem Projekt vorliegt."
},
"conversation-thread.reply-modal.header": {
"message": "Auf Thread antworten"
},
"conversation-thread.reply-modal.help-center-note": {
"message": "Wenn du mit dem Moderationsteam in Kontakt treten musst, verwende bitte das <help-center-link>Hilfezentrum von Modrinth</help-center-link> und klicke auf die blaue Sprechblase unten rechts, um den Support zu kontaktieren."
},
"conversation-thread.resubmit-modal.confirmation.description": {
"message": "Bestätigen, dass ich auf die Nachrichten der Moderatoren eingegangen bin"
},
"conversation-thread.resubmit-modal.confirmation.label": {
"message": "Ich bestätige, dass ich die Kommentare der Moderatoren ordnungsgemäss bearbeitet habe."
},
"conversation-thread.resubmit-modal.description": {
"message": "Du schickst <project-title>{projectTitle}</project-title> zur Wiederprüfung."
"message": "Du reichst <project-title>{projectTitle}</project-title> ein, um es erneut von den Moderatoren überprüfen zu lassen."
},
"conversation-thread.resubmit-modal.header.resubmitting": {
"message": "Erneut zur Überprüfung einreichen"
},
"conversation-thread.resubmit-modal.header.submitting": {
"message": "Erneut zur Überprüfung einreichen"
"message": "Zur Überprüfung einreichen"
},
"conversation-thread.resubmit-modal.reminder": {
"message": "Stelle sicher, dass du auf alle Kommentare des Moderationsteams eingegangen bist."
},
"conversation-thread.resubmit-modal.warning": {
"message": "Wiederholte Einreichungen ohne Berücksichtigung der Kommentare der Moderatoren können zur Sperrung deines Kontos führen."
},
"create-project-version.create-modal.stage.add-files.admonition": {
"message": "Ergänzende Dateien dienen zur unterstützung von Ressourcen wie Quellcode, nicht für alternative Versionen oder Varianten."
@@ -1398,13 +1494,13 @@
"message": "Alle als gelesen markieren"
},
"dashboard.overview.notifications.button.view-history": {
"message": "Verlauf ansehen"
"message": "Verlauf anzeigen"
},
"dashboard.overview.notifications.empty.no-unread": {
"message": "Du hast keine ungelesenen Benachrichtigungen."
},
"dashboard.overview.notifications.error.loading": {
"message": "Fehler beim Laden von Benachrichtigungen:"
"message": "Fehler beim Laden der Benachrichtigungen:"
},
"dashboard.overview.notifications.history.label": {
"message": "Verlauf"
@@ -1475,6 +1571,9 @@
"dashboard.projects.links.source-code.label": {
"message": "Quellcode"
},
"dashboard.projects.links.wiki-page.description": {
"message": "Eine Seite mit Informationen, Dokumentation und Hilfe zum Projekt"
},
"dashboard.projects.links.wiki-page.label": {
"message": "Wikiseite"
},
@@ -1736,14 +1835,20 @@
"discover.install.error.no-server-world": {
"message": "Keine Serverwelt ist zur installation verfügbar."
},
"discover.install.error.unsupported-content-type": {
"message": "Dieser Inhaltstyp kann nicht über Entdecken auf einem Server installiert werden."
},
"discover.install.heading.reset-modpack": {
"message": "Modpack zum installieren nach dem Reset auswählen"
},
"discover.seo.description": {
"message": "Durchsuche und durchstöbere Tausende von Minecraft-{projectType} auf Modrinth mit sofortigen, präzisen Suchergebnissen. Unsere Filter helfen dir, schnell die besten Minecraft-{projectType} zu finden."
},
"discover.seo.title": {
"message": "Durchsuche {projectType}"
"message": "{projectType} durchsuchen"
},
"discover.seo.title-with-query": {
"message": "Durchsuche {projectType} {query}"
"message": "{projectType} durchsuchen | {query}"
},
"error.collection.404.list_item.1": {
"message": "Du hast eventuell die Kollektions-URL falsch geschrieben."
@@ -1848,7 +1953,7 @@
"message": "Frosch"
},
"hosting-marketing.available-locations": {
"message": "Verfügbar in Nordamerika, Europa und Südostasien für eine breite Abdeckung."
"message": "Verfügbar in Nordamerika, Europa, Südostasien und Australien für eine breite Abdeckung."
},
"hosting-marketing.billing.monthly": {
"message": "Monatlich zahlen"
@@ -1910,6 +2015,9 @@
"hosting-marketing.faq.location": {
"message": "Wo befinden sich Server von Modrinth Hosting? Kann ich eine Region auswählen?"
},
"hosting-marketing.faq.location.answer": {
"message": "Wir haben derzeit Server in Nordamerika, Europa, Südostasien und Australien, die du beim Kauf auswählen kannst. Weitere Regionen folgen in der Zukunft! Wenn du deine Region wechseln möchtest, wende dich bitte an den Support."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Welche Minecraft-Versionen und Loader können verwendet werden?"
},
@@ -2252,6 +2360,9 @@
"layout.banner.build-fail.title": {
"message": "Fehler beim generieren des Status von der API beim erstellen."
},
"layout.banner.preview.description": {
"message": "Falls du die offizielle Modrinth-Website aufrufen wolltest, besuche {url}. Diese Preview-Bereitstellung wird von Modrinth-Mitarbeitern zu Testzwecken verwendet. Sie wurde mit {ref} erstellt."
},
"layout.banner.preview.title": {
"message": "Dies ist eine Vorschauversion der Modrinth-Webseite."
},
@@ -2726,6 +2837,9 @@
"muralpay.warning.wallet-address": {
"message": "Überprüfe deine Wallet-Adresse. An eine falsche Adresse gesendete Gelder können nicht wiederhergestellt werden."
},
"organization.settings.projects.edit-links.affected-projects": {
"message": "Die Änderungen werden auf {count, plural, one {# Projekt} other {# Projekte}} angewendet."
},
"profile.bio.fallback.creator": {
"message": "Ein Modrinth Ersteller."
},
@@ -2750,6 +2864,9 @@
"profile.button.set-affiliate": {
"message": "Als Partner setzen"
},
"profile.collection.projects-count": {
"message": "{count, plural, one {# Projekt} other {# Projekte}}"
},
"profile.details.label.auth-providers": {
"message": "Authentifizierungsanbieter"
},
@@ -2819,6 +2936,12 @@
"profile.meta.description-with-bio": {
"message": "{bio} - Lade {username}'s Projekte auf Modrinth herunter"
},
"profile.official-account": {
"message": "Offizielles Modrinth-Konto"
},
"profile.official-account.bio": {
"message": "Das offizielle Benutzerkonto von Modrinth. Erhalte Support unter <support-link></support-link> oder per E-Mail an <email></email>"
},
"profile.stats.projects-followers": {
"message": "{count, plural, one {<stat>{count}</stat>} other {<stat>{count}</stat>}} Projekt-Follower"
},
@@ -3083,41 +3206,77 @@
"project.moderation.admonition.approved.body.private": {
"message": "Dein Projekt ist privat, was bedeutet, dass nur du und von dir eingeladene Personen darauf zugreifen können."
},
"project.moderation.admonition.approved.body.public": {
"message": "Dein Projekt ist veröffentlicht und auf Modrinth auffindbar."
},
"project.moderation.admonition.approved.body.unlisted": {
"message": "Dein Projekt ist nicht gelistet, was bedeutet, dass man nur über einen direkten Link darauf zugreifen kann und es auf Modrinth nicht auffindbar ist."
},
"project.moderation.admonition.approved.body.visibility-message": {
"message": "Du kannst die Sichtbarkeit deines Projekts in den <visibility-settings-link>Sichtbarkeitseinstellungen</visibility-settings-link> deines Projekts ändern."
},
"project.moderation.admonition.approved.header": {
"message": "Projekt angenommen"
"message": "Projekt genehmigt"
},
"project.moderation.admonition.draft.body": {
"message": "Dies ist ein Entwurfsprojekt, das für andere erst sichtbar ist, nachdem es zur Überprüfung eingereicht und vom Modrinth-Moderationsteam genehmigt wurde."
},
"project.moderation.admonition.draft.header": {
"message": "Projektentwurf"
"message": "Entwurfsprojekt"
},
"project.moderation.admonition.draft.submit-for-review": {
"message": "Sobald du alle erforderlichen Schritte abgeschlossen und sichergestellt hast, dass dein Projekt den <rules-link>Inhaltsregeln</rules-link> von Modrinth entspricht, kannst du dein Projekt zur Überprüfung einreichen."
},
"project.moderation.admonition.rejected.address-all-concerns": {
"message": "Bitte gehe auf alle Moderationsanliegen ein, einschliesslich aller Probleme, die in den unten aufgeführten Nachrichten genannt werden, bevor du dieses Projekt erneut einreichst."
},
"project.moderation.admonition.rejected.header": {
"message": "Änderungen angefragt"
"message": "Änderungen angefordert"
},
"project.moderation.admonition.rejected.spam-notice": {
"message": "Das wiederholte Einreichen deines Projekts, ohne zuvor alle Moderationsanliegen zu klären, kann zur Sperrung deines Kontos führen."
},
"project.moderation.admonition.under-review.body.1": {
"message": "Dein Projekt ist in der Warteschlange, um von Modrinths Moderationsteam überprüft zu werden."
"message": "Dein Projekt befindet sich in der Warteschlange und wird vom Modrinth-Moderationsteam überprüft."
},
"project.moderation.admonition.under-review.body.2": {
"message": "Dein Projekt wird gescannt und anschliessend von menschlichen Moderatoren überprüft, um sicherzustellen, dass es den <rules-link>Inhaltsregeln</rules-link> und <terms-link>Nutzungsbedingungen</terms-link> von Modrinth entspricht."
},
"project.moderation.admonition.under-review.body.3": {
"message": "Du kannst dein Projekt weiterhin ändern, dies hat keinen Einfluss auf deine Position in der Warteschlange."
},
"project.moderation.admonition.under-review.body.4": {
"message": "Wir streben an, Einreichungen innerhalb von 24 bis 48 Stunden zu überprüfen, aber bei einigen Projekten kann es zu Verzögerungen kommen. Dies deutet nicht auf ein Problem mit deiner Einreichung hin."
},
"project.moderation.admonition.under-review.body.5": {
"message": "<emphasis>Wir bedanken uns für ihre Geduld während unsere Moderatoren hart dafür arbeiten Modrinth sicher zu halten und wir freuen und ihnen zu helfen ihre Kreationen zu teilen! 💚</emphasis>"
"message": "<emphasis>Wir schätzen deine Geduld, während unsere Moderatoren hart daran arbeiten, Modrinth sicher zu halten, und freuen uns darauf, dir dabei zu helfen, deine Inhalte zu teilen! 💚</emphasis>"
},
"project.moderation.admonition.under-review.header": {
"message": "Projekt wird überprüft"
},
"project.moderation.admonition.withheld.body": {
"message": "Ihr Projekt wird nicht öffentlich anzeigt und kann nur per Direkt Link angezeigt werden.{requestedStatus, select, unlisted { Laut deiner <visibility-settings-link>Anzeige Einstellung</visibility-settings-link>, Ist keine Aktion nötig.} other { Bitte behebe alle Moderationsprobleme, einschließlich aller in den folgenden Nachrichten genannten Punkte, bevor du dieses Projekt erneut einreichst.}}"
"message": "Dein Projekt wird nicht öffentlich erscheinen und kann nur über einen direkten Link aufgerufen werden. {requestedStatus, select, unlisted { Basierend auf deinen ausgewählten <visibility-settings-link>Sichtbarkeitseinstellungen</visibility-settings-link> ist höchstwahrscheinlich keine Aktion erforderlich.} other { Bitte gehe auf alle Moderationsanliegen ein, einschliesslich aller Probleme, die in den unten aufgeführten Nachrichten genannt werden, bevor du dieses Projekt erneut einreichst.}}"
},
"project.moderation.admonition.withheld.header": {
"message": "Von Mitarbeitern entlistet"
"message": "Durch Mitarbeiter nicht gelistet"
},
"project.moderation.error.unauthorized": {
"message": "Nicht autorisiert"
},
"project.moderation.thread.approved-warning": {
"message": "Dieser Thread wird nicht aktiv überwacht, kann aber bei Bedarf zur Information über dein Projekt überprüft werden."
},
"project.moderation.thread.help-center-note.1": {
"message": "Inhaltsmoderatoren können bei den meisten Problemen keinen Support leisten, und Nachrichten in diesem Thread benachrichtigen keine Mitarbeiter."
},
"project.moderation.thread.help-center-note.2": {
"message": "Falls du Hilfe brauchst oder du hast andere Anfragen, besuche bitte das <help-center-link>Modrinth Hilfe Center</help-center-link> und klicke auf die blaue Sprechblase um den Kundensupport zu kontaktieren."
"message": "Wenn du Hilfe benötigst oder weitere Fragen hast, besuche bitte das <help-center-link>Hilfezentrum von Modrinth</help-center-link> und klicke auf die blaue Sprechblase, um den Support zu kontaktieren."
},
"project.moderation.thread.moderator-see-user-ui-toggle": {
"message": "Zeige Benutzer UI"
"message": "Benutzer-UI anzeigen"
},
"project.moderation.thread.private-description": {
"message": "Dies ist eine private Konversation mit den Modrinth Moderatoren. Sie könnten dich bezüglich Problemen im Zusammenhang mit diesem Projekt kontaktieren."
"message": "Dies ist ein privater Unterhaltungsthread mit den Modrinth-Moderatoren. Sie können dich bei Problemen bezüglich dieses Projekts kontaktieren."
},
"project.moderation.thread.title": {
"message": "Moderationsnachrichten"
@@ -3233,6 +3392,9 @@
"project.stats.downloads-label": {
"message": "{count, plural, one {Download} other {Downloads}}"
},
"project.stats.followers-label": {
"message": "{count, plural, one {Follower} other {Follower}}"
},
"project.status.archived.message": {
"message": "{title} wurde archiviert. {title} wird keine weiteren Updates erhalten, es sei denn, der Autor entscheided sich, das Projekt zu Dearchivieren."
},
@@ -3251,6 +3413,9 @@
"report.already-reported": {
"message": "Du hast bereits {title} gemeldet"
},
"report.already-reported-description": {
"message": "Du hast bereits eine offene Meldung für dieses {item}. Du kannst deinem Bericht weitere Details hinzufügen, falls du weitere Informationen hast."
},
"report.back-to-item": {
"message": "Zurück zu {item}"
},
@@ -3264,7 +3429,7 @@
"message": "Überprüfe {item}..."
},
"report.could-not-find": {
"message": "Konnte {item} nicht finden"
"message": "{item} konnte nicht gefunden werden"
},
"report.for.violation": {
"message": "Verstoss gegen Modrinth's <rules-link>Regeln</rules-link> oder <terms-link>Nutzungsbedingungen</terms-link>"
@@ -3306,13 +3471,13 @@
"message": "Bitte melde:"
},
"report.question.content-id": {
"message": "Was ist die ID von dem {item}?"
"message": "Wie lautet die ID von {item}?"
},
"report.question.content-type": {
"message": "Welche Art an Inhalt meldest du?"
},
"report.question.report-reason": {
"message": "Gegen welche von Modrinths Regeln verstößt dieses {item}?"
"message": "Gegen welche von Modrinths Regeln verstösst dieses {item}?"
},
"report.report-content": {
"message": "Inhalt an Moderatoren melden"
@@ -3426,25 +3591,25 @@
"message": "Schreibzugriff Organisationen"
},
"scopes.patCreate.description": {
"message": "Persönliche Zugriffstoken erstellen"
"message": "Persönliche Zugriffs-Tokens erstellen"
},
"scopes.patCreate.label": {
"message": "PATs erstellen"
},
"scopes.patDelete.description": {
"message": "Deine persönlichen Zugriffstoken löschen"
"message": "Deine persönlichen Zugriffs-Tokens löschen"
},
"scopes.patDelete.label": {
"message": "PATs löschen"
},
"scopes.patRead.description": {
"message": "Erstellte persönliche Zugriffstoken ansehen"
"message": "Erstellte persönliche Zugriffs-Tokens anzeigen"
},
"scopes.patRead.label": {
"message": "Lesezugriff PATs"
},
"scopes.patWrite.description": {
"message": "Persönliche Zugriffstoken bearbeiten"
"message": "Persönliche Zugriffs-Tokens bearbeiten"
},
"scopes.patWrite.label": {
"message": "Schreibzugriff PATs"
@@ -3653,6 +3818,9 @@
"servers.plan.small.description": {
"message": "Perfekt für 1 - 5 Freunde mit ein par leichten Mods."
},
"servers.purchase.step.plan.billed": {
"message": "abgerechnet {interval, select, monthly {monthly} quarterly {quarterly} yearly {yearly} other {{interval}}}"
},
"settings.account.button.complete-setup": {
"message": "Einrichtung abschliessen"
},
@@ -4154,9 +4322,18 @@
"settings.billing.subscription.title": {
"message": "Abonnements"
},
"settings.billing.switch.switching-to-interval": {
"message": "Wechsel zu {interval, select, monthly {monthly} yearly {yearly} other {{interval}}}"
},
"settings.billing.switch.to-interval": {
"message": "Zu {interval, select, monthly {monthly} yearly {yearly} other {{interval}}} wechseln"
},
"settings.billing.switch.tooltip.monthly-additional-per-year": {
"message": "Monatliche Abrechnung kostet dich zusätzliche {amount} pro Jahr"
},
"settings.billing.switches-to-billing-on": {
"message": "Wechselt am {date} zur {interval, select, monthly {monthly} yearly {yearly} other {{interval}}} Abrechnung"
},
"settings.billing.update-method": {
"message": "Methode aktualisieren"
},
+210 -36
View File
@@ -74,20 +74,29 @@
"analytics.chart.controls.annotations": {
"message": "Anmerkungen"
},
"analytics.chart.controls.aria": {
"message": "Steuerelemente für Analysediagramm, {activeCount}"
},
"analytics.chart.controls.button": {
"message": "Einstellungen"
},
"analytics.chart.controls.dialog-aria": {
"message": "Analyse Graph Kontrolle"
"message": "Steuerelemente für Analysediagramm"
},
"analytics.chart.controls.display": {
"message": "Anzeige"
"message": "Anzeigen"
},
"analytics.chart.controls.modrinth-events": {
"message": "Modrinth Ereignisse"
"message": "Modrinth-Ereignisse"
},
"analytics.chart.controls.no-modrinth-events": {
"message": "Keine Modrinth Ereignisse im Graph."
"message": "Keine Modrinth-Ereignisse im Graphen."
},
"analytics.chart.controls.no-project-events": {
"message": "Keine Projektereignisse im Graph."
"message": "Keine Projektereignisse im Graphen."
},
"analytics.chart.controls.previous-period": {
"message": "Vorheriger Zeitraum"
},
"analytics.chart.controls.project-events": {
"message": "Projektereignisse"
@@ -95,6 +104,9 @@
"analytics.chart.controls.ratio": {
"message": "Verhältnis"
},
"analytics.chart.empty.select-table-items": {
"message": "Wähle Elemente aus der Tabelle unten aus, um deine Daten zu visualisieren."
},
"analytics.chart.events.count-aria": {
"message": "{count, plural, one {# Analyseereignis} other {# Analyseereignisse}}"
},
@@ -105,25 +117,34 @@
"message": "Ankündigung ansehen"
},
"analytics.chart.legend.monetization-details.aria": {
"message": "Zeige monetarisierte Analyse Details"
"message": "Details zu den monetarisierten Analysedaten anzeigen"
},
"analytics.chart.legend.monetization-details.description": {
"message": "Nur Ansichten und Downloads, die über Modrinth erfolgen, werden für die Monetarisierung gezählt, für Downloads müssen Nutzer angemeldet sein."
},
"analytics.chart.legend.monetization-details.title": {
"message": "Details zur Analyse der Monetarisierung"
},
"analytics.chart.legend.previous-period-suffix": {
"message": "{name} (Vorherige)"
"message": "{name} (Vorherig)"
},
"analytics.chart.render-limit.description": {
"message": "Anzeigen aller ausgewählten Zeilen von der Liste könnte die Performance beeinflussen."
"message": "Das Anzeigen aller ausgewählten Zeilen aus der Tabelle kann die Leistung der Seite beeinträchtigen."
},
"analytics.chart.render-limit.header": {
"message": "Alle {count} Zeilen im Graph anzeigen?"
"message": "Alle {count} Zeilen im Graphen anzeigen?"
},
"analytics.chart.table-selection.all": {
"message": "Zeige alle {itemType, select, project {{count, plural, one {Projekt} other {Projekte}}} country {{count, plural, one {Land} other {Länder}}} monetization {{count, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{count, plural, one {Downloadquelle} other {Downloadquellen}}} downloadReason {{count, plural, one {Downloadgrund} other {Downloadgründe}}} projectVersion {{count, plural, one {Projektversion} other {Projektversionen}}} loader {{count, plural, one {Loader} other {Loader}}} gameVersion {{count, plural, one {Spielversion} other {Spielversionen}}} other {{count, plural, one {Artikel} other {Artikel}}}} aus der Tabelle"
},
"analytics.chart.table-selection.count": {
"message": "Zeige {count} {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} der Liste"
"message": "Zeige {count} {itemType, select, project {{count, plural, one {Projekt} other {Projekte}}} country {{count, plural, one {Land} other {Länder}}} monetization {{count, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{count, plural, one {Downloadquelle} other {Downloadquellen}}} downloadReason {{count, plural, one {Downloadgrund} other {Downloadgründe}}} projectVersion {{count, plural, one {Projektversion} other {Projektversionen}}} loader {{count, plural, one {Loader} other {Loader}}} gameVersion {{count, plural, one {Spielversion} other {Spielversionen}}} other {{count, plural, one {Artikel} other {Artikel}}}} aus der Tabelle"
},
"analytics.chart.table-selection.limited": {
"message": "Zeige {limit} {itemType, select, project {{limit, plural, one {project} other {projects}}} country {{limit, plural, one {country} other {countries}}} monetization {{limit, plural, one {monetization value} other {monetization values}}} downloadSource {{limit, plural, one {download source} other {download sources}}} downloadReason {{limit, plural, one {download reason} other {download reasons}}} projectVersion {{limit, plural, one {project version} other {project versions}}} loader {{limit, plural, one {loader} other {loaders}}} gameVersion {{limit, plural, one {game version} other {game versions}}} other {{limit, plural, one {item} other {items}}}} der Liste"
"message": "Zeige {limit} {itemType, select, project {{limit, plural, one {Projekt} other {Projekte}}} country {{limit, plural, one {Land} other {Länder}}} monetization {{limit, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{limit, plural, one {Downloadquelle} other {Downloadquellen}}} downloadReason {{limit, plural, one {Downloadgrund} other {Downloadgründe}}} projectVersion {{limit, plural, one {Projektversion} other {Projektversionen}}} loader {{limit, plural, one {Loader} other {Loader}}} gameVersion {{limit, plural, one {Spielversion} other {Spielversionen}}} other {{limit, plural, one {Artikel} other {Artikel}}}} aus der Tabelle"
},
"analytics.chart.table-selection.top": {
"message": "Zeige top {count} {itemType, select, project {{count, plural, one {project} other {projects}}} country {{count, plural, one {country} other {countries}}} monetization {{count, plural, one {monetization value} other {monetization values}}} downloadSource {{count, plural, one {download source} other {download sources}}} downloadReason {{count, plural, one {download reason} other {download reasons}}} projectVersion {{count, plural, one {project version} other {project versions}}} loader {{count, plural, one {loader} other {loaders}}} gameVersion {{count, plural, one {game version} other {game versions}}} other {{count, plural, one {item} other {items}}}} von der Liste"
"message": "Zeige die top {count} {itemType, select, project {{count, plural, one {Projekt} other {Projekte}}} country {{count, plural, one {Land} other {Länder}}} monetization {{count, plural, one {Monetarisierungswert} other {Monetarisierungswerte}}} downloadSource {{count, plural, one {Downloadquelle} other {Downloadquellen}}} downloadReason {{count, plural, one {Downloadgrund} other {Downloadgründe}}} projectVersion {{count, plural, one {Projektversion} other {Projektversionen}}} loader {{count, plural, one {Loader} other {Loader}}} gameVersion {{count, plural, one {Spielversion} other {Spielversionen}}} other {{count, plural, one {Artikel} other {Artikel}}}} aus der Tabelle"
},
"analytics.chart.tooltip.duration.days": {
"message": "{count, plural, one {# Tag} other {# Tage}}"
@@ -137,11 +158,17 @@
"analytics.chart.tooltip.hide-entry": {
"message": "{name} im Diagramm ausblenden"
},
"analytics.chart.tooltip.pinned": {
"message": "Infofenster des Diagramms fixiert"
},
"analytics.chart.tooltip.pinned-aria": {
"message": "Angepinnt"
},
"analytics.chart.tooltip.previous-period-short": {
"message": "(Vorherige)"
"message": "(Vorherig)"
},
"analytics.chart.tooltip.show-entry": {
"message": "{name} im Diagramm anzeigen"
},
"analytics.chart.tooltip.total": {
"message": "Gesamt"
@@ -180,7 +207,7 @@
"message": "Keine Daten verfügbar"
},
"analytics.empty.no-data-for-analytics": {
"message": "Keine Daten verfügbar für diese Analysen"
"message": "Keine Daten für die Analyse verfügbar"
},
"analytics.empty.no-projects": {
"message": "Keine Projekte verfügbar"
@@ -189,16 +216,16 @@
"message": "Keine Projekte für die Analyse verfügbar"
},
"analytics.empty.select-project": {
"message": "Wähle mindestens ein Projekt aus um Daten anzuzeigen"
"message": "Wählen mindestens ein Projekt aus, um Daten anzuzeigen"
},
"analytics.filter.game-version-type": {
"message": "Spiel Version wählen"
"message": "Art der Spielversion"
},
"analytics.filter.game-version-type.all": {
"message": "Alle"
},
"analytics.filter.game-version-type.release": {
"message": "Veröffentlichung"
"message": "Vollversion"
},
"analytics.filter.search.countries": {
"message": "Länder durchsuchen..."
@@ -318,7 +345,7 @@
"message": "{count, plural, one {# Projekt} other {# Projekte}}"
},
"analytics.project.icon-alt": {
"message": "{name} Icon"
"message": "Icon von {name}"
},
"analytics.project.select": {
"message": "Projekte auswählen"
@@ -336,11 +363,20 @@
"message": "Projekt:"
},
"analytics.query.label.timeframe": {
"message": "Zeitfenster:"
"message": "Zeitraum:"
},
"analytics.stat.downloads": {
"message": "Downloads"
},
"analytics.stat.monetization-banner.body": {
"message": "Nur Ansichten und Downloads, die über Modrinth erfolgen, sind monetarisierbar und müssen die Filter zur Betrugsbekämpfung bestehen. Für Downloads über die Modrinth App muss der Nutzer zudem angemeldet sein. Da alle Projekte einen ähnlichen Anteil an monetarisierten Downloads aufweisen, würden sich deine Einnahmen nicht wesentlich ändern, wenn alle Downloads gezählt würden."
},
"analytics.stat.monetization-banner.learn-more": {
"message": "Mehr erfahren"
},
"analytics.stat.monetization-banner.title": {
"message": "Wie funktioniert die Monetarisierung?"
},
"analytics.stat.playtime": {
"message": "Spielzeit"
},
@@ -363,7 +399,7 @@
"message": "N/A"
},
"analytics.stat.unavailable-tooltip": {
"message": "Statistik nicht verfügbar für aktuellen Filter"
"message": "Statistik für die aktuellen Filter nicht verfügbar"
},
"analytics.stat.views": {
"message": "Ansichten"
@@ -371,9 +407,15 @@
"analytics.table.csv.date-range": {
"message": "{start} bis {end}"
},
"analytics.table.csv.filename": {
"message": "Modrinth Analysen Auswertung nach {breakdown} - {dateRange}"
},
"analytics.table.csv.header.playtime-seconds": {
"message": "Spielzeit (Sekunden)"
},
"analytics.table.csv.selected-range": {
"message": "Ausgewählter Bereich"
},
"analytics.table.duration.days": {
"message": "{count, plural, one {# Tag} other {# Tage}}"
},
@@ -383,6 +425,9 @@
"analytics.table.duration.minutes": {
"message": "{count, plural, one {# Minute} other {# Minuten}}"
},
"analytics.table.empty.no-matching-rows": {
"message": "Keine übereinstimmenden Analysezeilen"
},
"analytics.table.export-csv": {
"message": "CSV exportieren"
},
@@ -849,7 +894,7 @@
"message": "Mit Antwort annehmen"
},
"conversation-thread.action.close-thread": {
"message": ""
"message": "Thread schließen"
},
"conversation-thread.action.close-with-reply": {
"message": "Mit Antwort schließen"
@@ -861,22 +906,34 @@
"message": "Mit Antwort ablehnen"
},
"conversation-thread.action.reopen-thread": {
"message": "Öffne den Thread wieder"
"message": "Thread erneut öffnen"
},
"conversation-thread.action.reply": {
"message": "Antworten"
},
"conversation-thread.action.reply-to-thread": {
"message": "Antworte dem Thread"
"message": "Dem Thread antworten"
},
"conversation-thread.action.resubmit-for-review": {
"message": "Erneut zur Überprüfung einreichen"
},
"conversation-thread.action.resubmit-for-review-with-reply": {
"message": "Zur erneuten Überprüfung mit Antwort einreichen"
},
"conversation-thread.action.send": {
"message": "Senden"
},
"conversation-thread.action.send-to-review": {
"message": "Absenden zur Überprüfung"
"message": "Zur Überprüfung einsenden"
},
"conversation-thread.action.send-to-review-with-reply": {
"message": "Zur Überprüfung mit Antwort senden"
},
"conversation-thread.action.set-to-draft": {
"message": "Als Entwurf speichern"
},
"conversation-thread.action.set-to-draft-with-reply": {
"message": "Als Entwurf mit Antwort speichern"
},
"conversation-thread.action.withhold": {
"message": "Zurückhalten"
@@ -884,12 +941,45 @@
"conversation-thread.action.withhold-with-reply": {
"message": "Mit Antwort zurückhalten"
},
"conversation-thread.closed-thread.description": {
"message": "Dieser Thread ist geschlossen und keine neuen Nachrichten können mehr gesendet werden."
},
"conversation-thread.error.closing-report": {
"message": "Fehler beim Schließen der Meldung"
},
"conversation-thread.error.reopening-report": {
"message": "Fehler beim erneuten Öffnen der Meldung"
},
"conversation-thread.error.sending-message": {
"message": "Fehler beim Senden der Nachricht"
},
"conversation-thread.reply-editor.placeholder.reply": {
"message": "Dem Thread antworten..."
},
"conversation-thread.reply-editor.placeholder.send": {
"message": "Sende eine Nachricht..."
},
"conversation-thread.reply-modal.confirmation.description": {
"message": "Bestätige, dass die Moderatoren dies nicht aktiv überwachen"
},
"conversation-thread.reply-modal.confirmation.label": {
"message": "Mir ist bewusst, dass dieser Thread nicht aktiv von den Moderatoren überwacht wird."
},
"conversation-thread.reply-modal.description": {
"message": "Dein Projekt wurde bereits angenommen. Daher wird dieser Thread vom Moderationsteam nicht aktiv überwacht. Sollte es jedoch ein Problem mit deinem Projekt geben, kann es sein, dass die Moderatoren deine Nachricht dennoch sehen."
},
"conversation-thread.reply-modal.header": {
"message": "Dem Thread antworten"
},
"conversation-thread.reply-modal.help-center-note": {
"message": "Falls du das Moderationsteam kontaktieren möchtest, nutze bitte das <help-center-link>Hilfezentrum von Modrinth</help-center-link> und klicke auf die blaue Sprechblase unten rechts, um den Support zu kontaktieren."
},
"conversation-thread.resubmit-modal.confirmation.description": {
"message": "Bestätige, dass die Anmerkungen der Moderatoren berücksichtigt wurden"
},
"conversation-thread.resubmit-modal.confirmation.label": {
"message": "Ich bestätige, dass ich die Anmerkungen der Moderatoren entsprechend berücksichtigt habe."
},
"conversation-thread.resubmit-modal.description": {
"message": "Du schickst <project-title>{projectTitle}</project-title> zur Wiederprüfung."
},
@@ -899,6 +989,12 @@
"conversation-thread.resubmit-modal.header.submitting": {
"message": "Erneut zur Überprüfung einreichen"
},
"conversation-thread.resubmit-modal.reminder": {
"message": "Stelle sicher, dass du alle Anmerkungen des Moderationsteams berücksichtigt hast."
},
"conversation-thread.resubmit-modal.warning": {
"message": "Wiederholte Einreichungen, ohne auf die Anmerkungen der Moderatoren einzugehen, können zu einer Sperrung des Kontos führen."
},
"create-project-version.create-modal.stage.add-files.admonition": {
"message": "Ergänzende Dateien dienen zur Unterstützung von Ressourcen wie Quellcode, nicht für alternative Versionen oder Varianten."
},
@@ -1475,6 +1571,9 @@
"dashboard.projects.links.source-code.label": {
"message": "Quellcode"
},
"dashboard.projects.links.wiki-page.description": {
"message": "Eine Seite mit Informationen, Dokumentation und Hilfe zum Projekt"
},
"dashboard.projects.links.wiki-page.label": {
"message": "Wikiseite"
},
@@ -1736,9 +1835,15 @@
"discover.install.error.no-server-world": {
"message": "Keine Serverwelt ist zur Installation verfügbar."
},
"discover.install.error.unsupported-content-type": {
"message": "Dieser Inhaltstyp kann über Entdecken nicht auf einem Server installiert werden."
},
"discover.install.heading.reset-modpack": {
"message": "Modpack zur Installation nach dem Zurücksetzen auswählen"
},
"discover.seo.description": {
"message": "Durchsuche und entdecke auf Modrinth Tausende von {projectType} für Minecraft mit sofortigen, präzisen Suchergebnissen. Mit unseren Filtern findest du schnell die besten {projectType} für Minecraft."
},
"discover.seo.title": {
"message": "Durchsuche {projectType}"
},
@@ -1848,7 +1953,7 @@
"message": "Frosch"
},
"hosting-marketing.available-locations": {
"message": "Verfügbar in Nordamerika, Europa und Südostasien für eine breite Abdeckung."
"message": "Verfügbar in Nordamerika, Europa, Südostasien und Australien für eine breite Abdeckung."
},
"hosting-marketing.billing.monthly": {
"message": "Monatlich zahlen"
@@ -1910,6 +2015,9 @@
"hosting-marketing.faq.location": {
"message": "Wo befinden sich Server von Modrinth Hosting? Kann ich eine Region auswählen?"
},
"hosting-marketing.faq.location.answer": {
"message": "Derzeit sind Server in Nordamerika, Europa, Südostasien und Australien verfügbar, die du beim Kauf auswählen kannst. Weitere Regionen werden in Zukunft dazukommen! Falls du deine Region wechseln möchtest, wende dich bitte an den Support."
},
"hosting-marketing.faq.versions-loaders": {
"message": "Welche Minecraft-Versionen und Loader können verwendet werden?"
},
@@ -2252,6 +2360,9 @@
"layout.banner.build-fail.title": {
"message": "Fehler beim Generieren des Status aus der API beim Erstellen."
},
"layout.banner.preview.description": {
"message": "Falls du die offizielle Modrinth-Website aufrufen wolltest, besuche {url}. Diese Vorschauversion wird von Modrinth-Mitarbeitern zu Testzwecken genutzt. Sie wurde mit {ref} erstellt."
},
"layout.banner.preview.title": {
"message": "Dies ist eine Vorschauversion der Modrinth-Website."
},
@@ -2726,6 +2837,9 @@
"muralpay.warning.wallet-address": {
"message": "Überprüfe deine Wallet-Adresse. An eine falsche Adresse gesendete Gelder können nicht wiederhergestellt werden."
},
"organization.settings.projects.edit-links.affected-projects": {
"message": "Änderungen werden auf {count, plural, one {# Projekt} other {# Projekte}} angewendet."
},
"profile.bio.fallback.creator": {
"message": "Ein Modrinth-Ersteller."
},
@@ -2750,6 +2864,9 @@
"profile.button.set-affiliate": {
"message": "Als Partner setzen"
},
"profile.collection.projects-count": {
"message": "{count, plural, one {# Projekt} other {# Projekte}}"
},
"profile.details.label.auth-providers": {
"message": "Authentifizierungsanbieter"
},
@@ -2819,6 +2936,12 @@
"profile.meta.description-with-bio": {
"message": "{bio} - Lade {username}s Projekte auf Modrinth herunter"
},
"profile.official-account": {
"message": "Offizielles Konto von Modrinth"
},
"profile.official-account.bio": {
"message": "Das offizielle Benutzerkonto von Modrinth. Erhalte Hilfe unter <support-link></support-link> oder per E-Mail unter <email></email>"
},
"profile.stats.projects-followers": {
"message": "{count, plural, one {<stat>{count}</stat>} other {<stat>{count}</stat>}} Projekt-Follower"
},
@@ -3083,26 +3206,56 @@
"project.moderation.admonition.approved.body.private": {
"message": "Dein Projekt ist privat, was bedeutet, dass nur du und von dir eingeladene Personen darauf zugreifen können."
},
"project.moderation.admonition.approved.body.public": {
"message": "Dein Projekt ist veröffentlicht und auf Modrinth auffindbar."
},
"project.moderation.admonition.approved.body.unlisted": {
"message": "Dein Projekt ist nicht gelistet, was bedeutet, dass es nur über einen direkten Link aufgerufen werden kann und auf Modrinth nicht auffindbar ist."
},
"project.moderation.admonition.approved.body.visibility-message": {
"message": "Du kannst die Sichtbarkeit deines Projekts in den <visibility-settings-link>Sichtbarkeitseinstellungen</visibility-settings-link> deines Projekts ändern."
},
"project.moderation.admonition.approved.header": {
"message": "Projekt angenommen"
},
"project.moderation.admonition.draft.body": {
"message": "Dies ist ein Projektentwurf, der für andere erst sichtbar wird, wenn er zur Überprüfung eingereicht und vom Moderationsteam von Modrinth genehmigt wurde."
},
"project.moderation.admonition.draft.header": {
"message": "Projektentwurf"
},
"project.moderation.admonition.draft.submit-for-review": {
"message": "Sobald du alle erforderlichen Schritte abgeschlossen und sichergestellt hast, dass dein Projekt den <rules-link>Inhaltsrichtlinien</rules-link> von Modrinth entspricht, kannst du dein Projekt zur Überprüfung einreichen."
},
"project.moderation.admonition.rejected.address-all-concerns": {
"message": "Bitte berücksichtige alle Moderationsbedenken, einschließlich aller in den folgenden Nachrichten aufgeführten Punkte, bevor du dieses Projekt erneut einreichst."
},
"project.moderation.admonition.rejected.header": {
"message": "Änderungen angefragt"
},
"project.moderation.admonition.rejected.spam-notice": {
"message": "Wiederholtes Einreichen deines Projekts, ohne zuvor alle Bedenken der Moderatoren zu berücksichtigen, kann zur Sperrung deines Kontos führen."
},
"project.moderation.admonition.under-review.body.1": {
"message": "Dein Projekt ist in der Warteschlange, um von Modrinths Moderationsteam überprüft zu werden."
},
"project.moderation.admonition.under-review.body.2": {
"message": "Dein Projekt wird gescannt und anschließend von menschlichen Moderatoren geprüft, um sicherzustellen, dass es den <rules-link>Inhaltsrichtlinien</rules-link> und <terms-link>Nutzungsbedingungen</terms-link> von Modrinth entspricht."
},
"project.moderation.admonition.under-review.body.3": {
"message": "Du kannst dein Projekt weiterhin bearbeiten, dies hat keinen Einfluss auf deinen Platz in der Warteschlange."
},
"project.moderation.admonition.under-review.body.4": {
"message": "Wir bemühen uns, Einsendungen innerhalb von 24 bis 48 Stunden zu überprüfen, doch bei einigen Projekten kann es zu Verzögerungen kommen. Dies bedeutet nicht, dass mit deiner Einreichung etwas nicht in Ordnung ist."
},
"project.moderation.admonition.under-review.body.5": {
"message": "<emphasis>Wir bedanken uns für ihre Geduld während unsere Moderatoren hart dafür arbeiten Modrinth sicher zu halten und wir freuen und ihnen zu helfen ihre Kreationen zu teilen! 💚</emphasis>"
"message": "<emphasis>Wir danken dir für deine Geduld, während unsere Moderatoren daran arbeiten, Modrinth sicher zu halten, und freuen uns darauf, dir dabei zu helfen, deine Inhalte zu teilen! 💚</emphasis>"
},
"project.moderation.admonition.under-review.header": {
"message": "Projekt wird überprüft"
},
"project.moderation.admonition.withheld.body": {
"message": "Ihr Projekt wird nicht öffentlich anzeigt und kann nur per Direkt Link angezeigt werden.{requestedStatus, select, unlisted { Laut deiner <visibility-settings-link>Anzeige Einstellung</visibility-settings-link>, Ist keine Aktion nötig.} other { Bitte behebe alle Moderationsprobleme, einschließlich aller in den folgenden Nachrichten genannten Punkte, bevor du dieses Projekt erneut einreichst.}}"
"message": "Dein Projekt wird nicht öffentlich angezeigt und kann nur über einen direkten Link aufgerufen werden.{requestedStatus, select, unlisted { Basierend auf deinen gewählten <visibility-settings-link>Sichtbarkeitseinstellungen</visibility-settings-link> sind höchstwahrscheinlich keine weiteren Maßnahmen erforderlich.} other { Bitte behebe vor dem erneuten Einreichen dieses Projekts alle Moderationshinweise, einschließlich der unten aufgeführten Probleme in den Nachrichten.}}"
},
"project.moderation.admonition.withheld.header": {
"message": "Von Mitarbeitern entlistet"
@@ -3110,14 +3263,20 @@
"project.moderation.error.unauthorized": {
"message": "Nicht autorisiert"
},
"project.moderation.thread.approved-warning": {
"message": "Dieser Thread wird nicht aktiv überwacht, kann aber bei Bedarf auf Informationen zu deinem Projekt überprüft werden."
},
"project.moderation.thread.help-center-note.1": {
"message": "Inhaltsmoderatoren können bei den meisten Problemen keine Unterstützung leisten, außerdem werden Mitarbeiter durch Nachrichten in diesem Thread nicht benachrichtigt."
},
"project.moderation.thread.help-center-note.2": {
"message": "Falls du Hilfe brauchst oder du hast andere Anfragen, besuche bitte das <help-center-link>Modrinth Hilfe Center</help-center-link> und klicke auf die blaue Sprechblase um den Kundensupport zu kontaktieren."
"message": "Falls du Hilfe benötigst oder weitere Fragen hast, besuche bitte das <help-center-link>Hilfezentrum von Modrinth</help-center-link> und klicke auf die blaue Sprechblase, um den Support zu kontaktieren."
},
"project.moderation.thread.moderator-see-user-ui-toggle": {
"message": "Zeige Benutzer UI"
"message": "Mitglieder-UI anzeigen"
},
"project.moderation.thread.private-description": {
"message": "Dies ist eine private Konversation mit den Modrinth Moderatoren. Sie könnten dich bezüglich Problemen im Zusammenhang mit diesem Projekt kontaktieren."
"message": "Dies ist eine private Konversation mit den Moderatoren von Modrinth. Diese können dir Nachrichten zu Problemen bezüglich dieses Projekts senden."
},
"project.moderation.thread.title": {
"message": "Moderationsnachrichten"
@@ -3251,6 +3410,9 @@
"report.already-reported": {
"message": "Du hast {title} bereits gemeldet"
},
"report.already-reported-description": {
"message": "Du hast bereits eine offene Meldung für dieses/n {item}. Du kannst weitere Details zu deiner Meldung hinzufügen, falls du weitere Informationen hast."
},
"report.back-to-item": {
"message": "Zurück zu {item}"
},
@@ -3258,7 +3420,7 @@
"message": "Füge nach Möglichkeit und Relevanz Links und Bilder ein. Leere oder unzureichende Meldungen werden geschlossen und ignoriert."
},
"report.body.title": {
"message": "Bitte gib weiteren Kontext über deine Meldung"
"message": "Bitte gib weiteren Kontext zu deiner Meldung"
},
"report.checking": {
"message": "Überprüfe {item}..."
@@ -3357,7 +3519,7 @@
"message": "Sitzungen"
},
"scopes.category.threads": {
"message": "Beiträge"
"message": "Threads"
},
"scopes.category.user-account": {
"message": "Benutzerkonto"
@@ -3534,16 +3696,16 @@
"message": "Sitzungen lesen"
},
"scopes.threadRead.description": {
"message": "Beiträge einsehen"
"message": "Threads einsehen"
},
"scopes.threadRead.label": {
"message": "Beiträge einsehen"
"message": "Threads einsehen"
},
"scopes.threadWrite.description": {
"message": "Beiträge bearbeiten"
"message": "Threads bearbeiten"
},
"scopes.threadWrite.label": {
"message": "Beiträge schreiben"
"message": "Threads schreiben"
},
"scopes.userAuthWrite.description": {
"message": "Deine Authentifizierungsdaten ändern"
@@ -3653,6 +3815,9 @@
"servers.plan.small.description": {
"message": "Perfekt für 15 Freunde mit ein paar kleinen Mods."
},
"servers.purchase.step.plan.billed": {
"message": "{interval, select, monthly {monatlich} quarterly {vierteljährlich} yearly {jährlich} other {{interval}}} abgerechnet"
},
"settings.account.button.complete-setup": {
"message": "Einrichtung abschließen"
},
@@ -4154,9 +4319,18 @@
"settings.billing.subscription.title": {
"message": "Abonnements"
},
"settings.billing.switch.switching-to-interval": {
"message": "Wird auf {interval, select, monthly {monatlich} yearly {jährlich} other {{interval}}} gewechselt"
},
"settings.billing.switch.to-interval": {
"message": "Wechseln auf {interval, select, monthly {monatlich} yearly {jährlich} other {{interval}}}"
},
"settings.billing.switch.tooltip.monthly-additional-per-year": {
"message": "Monatliche Abrechnung kostet dich zusätzliche {amount} pro Jahr"
},
"settings.billing.switches-to-billing-on": {
"message": "Wechselt am {date} auf {interval, select, monthly {monatliche} yearly {jährliche} other {{interval}}} Abrechnung"
},
"settings.billing.update-method": {
"message": "Methode aktualisieren"
},
+11 -2
View File
@@ -368,6 +368,15 @@
"analytics.stat.downloads": {
"message": "Downloads"
},
"analytics.stat.monetization-banner.body": {
"message": "Only views and downloads made through Modrinth are eligible for monetization and must pass fraud-prevention filtering. Modrinth App downloads also require the user to be logged in. Because all projects have a similar ratio of monetized downloads, your revenue would not meaningfully change if all downloads were counted."
},
"analytics.stat.monetization-banner.learn-more": {
"message": "Learn more"
},
"analytics.stat.monetization-banner.title": {
"message": "How does monetization work?"
},
"analytics.stat.playtime": {
"message": "Playtime"
},
@@ -1944,7 +1953,7 @@
"message": "Frog"
},
"hosting-marketing.available-locations": {
"message": "Available in North America, Europe, and Southeast Asia for wide coverage."
"message": "Available in North America, Europe, Southeast Asia, and Australia for wide coverage."
},
"hosting-marketing.billing.monthly": {
"message": "Pay monthly"
@@ -2007,7 +2016,7 @@
"message": "Where are Modrinth Hosting servers located? Can I choose a region?"
},
"hosting-marketing.faq.location.answer": {
"message": "We have servers available across North America, Europe, and Southeast Asia at the moment that you can choose upon purchase. More regions to come in the future! If you'd like to switch your region, please contact support."
"message": "We have servers available across North America, Europe, Southeast Asia, and Australia at the moment that you can choose upon purchase. More regions to come in the future! If you'd like to switch your region, please contact support."
},
"hosting-marketing.faq.versions-loaders": {
"message": "What Minecraft versions and loaders can be used?"

Some files were not shown because too many files have changed in this diff Show More