Compare commits

...
Author SHA1 Message Date
tdgao 4897cffb8d Merge branch 'boris/dev-908-backend-changes' of github.com:modrinth/code into boris/dev-908-backend-changes 2026-06-25 13:49:51 -06:00
tdgao 339beddcbc pnpm prepr 2026-06-25 13:49:49 -06:00
Truman GaoandGitHub 73232b3b7f Merge branch 'main' into boris/dev-908-backend-changes 2026-06-25 13:49:42 -06:00
tdgao bc073581d2 fix copy 2026-06-25 13:46:57 -06:00
tdgao 4eabe2a126 feat: improve manage passkeys UI 2026-06-25 13:45:09 -06:00
aecsocketandGitHub 6fc741f7c0 fix: file scan fix v2 (#6504)
* fix: file attribution scanning not marking completed files

* prepare

* Handle file scan failures gracefully with tracking

* conditional

* add more logging

* remove leftover files
2026-06-25 20:50:04 +02:00
tdgao 24285baebb fmt 2026-06-25 11:58:40 -06:00
Truman GaoandGitHub ae939cd4ca Merge branch 'main' into boris/dev-908-backend-changes 2026-06-25 11:13:28 -06:00
DeDiamondProandGitHub 8faa36b014 feat: add passkey support (#6375)
* feat: add passkey backend

* feat: passkey frontend

* invalidate sessions on compromised passkey

* chore: run sqlx prepare

* fix: make passkey button use both collumns to prevent empty space

* fix: correctly verify max passkeys in finish route

* fix: use structs for response

* fix: add rp name default

* style: use web::Json
2026-06-25 10:13:08 -07:00
aecsocketandGitHub 527757d158 fix: file attribution scanning not marking completed files (#6500)
* fix: file attribution scanning not marking completed files

* Clean up unused attribution files/groups on deletion

* prepare

* Handle file scan failures gracefully with tracking
2026-06-25 15:57:51 +00:00
aecsocketandGitHub f4a2999b20 Batch pending file scanning ops (#6491)
* Batch pending file scanning ops

* prepare

* move some stuff out of the txn

* scan concurrency

* prepr

* 🦀 automod attribution is dead, long live automod

* fix ci

* update scan file filtering
2026-06-24 21:46:07 +00:00
tdgao a45a320512 fix: add stroke 2026-06-24 12:48:10 -06:00
aecsocketandGitHub 586cdc90f9 fix: word separators in Typesense search fields regression (#6492) 2026-06-24 18:09:18 +00:00
Truman GaoandGitHub e56f3ae18a fix: package datapack as mod (#6494)
* feat: update package as mod copy

* fix: add environment field for v3

* pnpm prepr

* feat: use coloured tags for loaders in multiselect
2026-06-24 18:08:33 +00:00
tdgao 98c70633ee Merge branch 'main' into boris/dev-908-backend-changes 2026-06-24 11:05:06 -06:00
tdgao d858901568 Revert "Merge branch 'main' into boris/dev-908-backend-changes"
This reverts commit b9b03796e3.
2026-06-24 10:58:08 -06:00
tdgao b9b03796e3 Merge branch 'main' into boris/dev-908-backend-changes 2026-06-24 10:49:12 -06:00
aecsocketandGitHub c392c85917 perf: Attribution route perf improvements (#6489)
* Attribution route perf improvements

* fix clippy

* clear exemptions when user attempts to scan their file

* Increase Redis TTL/refresh time
2026-06-24 17:48:35 +02:00
Prospector 29110890e1 changelog 2026-06-24 00:22:41 -07:00
Prospector a18f67bf0c Merge branch 'main' into prospector/modpack-proj-fixes 2026-06-24 00:21:19 -07:00
Prospector 623e01dbff bunch more fixes lol 2026-06-24 00:20:02 -07:00
Prospector 14c728c8e3 Copy ID button 2026-06-23 23:03:16 -07:00
ThatGravyBoatandGitHub a1b2073f57 feat: add moderation status action button to tech review (#6484)
* feat: add moderation status action button to tech review

* chore: run prepr
2026-06-24 05:54:39 +00:00
François-Xavier TalbotandGitHub 45c417e406 fix query cache (#6488) 2026-06-24 06:42:14 +02:00
François-Xavier TalbotandGitHub 47c60788ff attributions fixes (#6487)
* feat(labrinth): begin & commit one transaction per file scans

* chore(labrinth): use ro pool for the query
2026-06-24 06:19:10 +02:00
Prospector 3c60cc6f08 project page gap 2026-06-23 15:20:19 -07:00
Michael H. d1e8a9c574 perf: split attribution_enforced_versions subquery to use indexes
The original view used a single correlated `not exists` with an `or`,
which prevented the partial unique indexes on `attributions_exemptions`
from being used. Splitting into two `not exists` clauses lets each
anti-join use its own index instead of a full sequential scan.
2026-06-24 00:19:16 +02:00
aecsocketandGitHub 7bf85239de Improve error handling in account balance notifications (#6478)
* Improve error handling in account balance notifications

* fix compile
2026-06-23 21:11:02 +00:00
Prospector 06353bbc14 move deps after compat 2026-06-23 13:50:35 -07:00
6380dd16d5 chore: modpack permissions blog (#6474)
* chore: add blog

* update bullets

* Update blog

* remove old files

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-06-23 20:24:38 +00:00
Prospector 2376d571bb changelog 2026-06-23 12:31:41 -07:00
e7926083fb feat: new modpack permissions system (#6005)
* Begin external projects moderator database frontend

* add copy link button

* begin project page permissions settings

* MEL database backend routes

* include filename in external files

* wip: when uploading a version file, fetch its overrides as a list

* wip: override license checks

* improve FileHost ref counting

* file host read capability

* scan files when inserting version file

* add dependency sha1 field

* clean up version files

* wip: attributions

* update s3 file host

* attribution scanning basic works

* works

* insert attribution info after resolving

* add routes

* remove dep sha1 stuff

* prepr

* wip: override file sources

* add files_missing_attributions to versions

* return extended version info + attributed at/by

* hook up frontend to backend (mostly)

* expose version date published

* withholding version visibility

* frontend work

* prepr

* use api-client for img upload

* moar frontend

* prepr

* Add schema to attribution resolution and Flame project results

* sqlx prepare

* changes

* remove feature flag, fix optional proof images

* fix schema

* fmt

* fix deletion and file fetch

* prepare

* fix admonition

* update frontend stuff to new schema

* prepr

* attribution on dependencies

* fixes

* sqlx prepare

* fixes

* routes

* fix routes

* Version grandfathering

* prepare

* wip: bulk routes

* pushing what i've got rn

* include link in NoPermission

* change hash insert to bulk route

* query flame even if entry in MEL

* delete file with weird name

* Prioritise putting override files in existing groups even with ExternalLicense

* fix how hex bytes are handled in route

* feat: coolbot moderation changes (#6215)

* Update moderator checklist

* move permissions stage order

* Updated nagContext.versions to v3, added nag for permissions

* Update permissions.vue default messages

* prepr

---------

Co-authored-by: coolbot100s <76798835+coolbot100s@users.noreply.github.com>

* QA

* prepr

* should group by project

* return attribution resolution correctly

* updated by moderator info

* Track what moderator reviewed an attribution moderation status

* default deser FMA field

* new version page

* clean up fetching + add a couple missing features

* qa items

* prepr

* provide moderation package stuff with DI

* format?

* don't redact moderated_at

* move supplementary resources

* Reorganize moderation messages.

* Quick replies for external content permissions.

* prepare

* QA

* allow exempting projects

* Ignore Flame projects which 404

* fix ci

* fix cross project attribution stuff

* Fix permission error

* change what files get cscanned

* add more logging

* QA Jun 22

* fix

* idempotency

* Expose route for rescanning

* update blog link

---------

Co-authored-by: aecsocket <aecsocket@tutanota.com>
Co-authored-by: coolbot100s <76798835+coolbot100s@users.noreply.github.com>
Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com>
2026-06-23 21:27:51 +02:00
tdgao e8475086ec fix: unused import 2026-06-23 13:19:40 -06:00
Michael H. a686a93858 fix: preview deployment incorrect branch data 2026-06-23 20:57:47 +02:00
tdgao 655f899785 Merge branch 'main' into boris/dev-908-backend-changes 2026-06-23 12:48:00 -06:00
tdgao 9bcee5ab52 feat: update sign in screen 2026-06-23 12:45:18 -06:00
Modrinth BotandGitHub 2876890fe5 New translations from Crowdin (main) (#6466) 2026-06-23 18:36:07 +00:00
tdgao 9f050f21e6 improve reset password 2026-06-23 12:03:59 -06:00
tdgao 782c82d3e8 update copy 2026-06-23 11:54:38 -06:00
tdgao da828f4db8 update copy 2026-06-23 11:46:50 -06:00
Calum H.andGitHub 4c869d2a37 fix: broken icu script (#6480) 2026-06-23 14:43:12 +00:00
ThatGravyBoatandGitHub 525f60c389 fix(docs): missing environment field from v2 createVersion route (#6476)
* fix(docs): missing environment field from v2 createVersion route

* fix(docs): missing environment field from v2 createVersion route
2026-06-23 00:35:41 +00:00
Truman GaoandGitHub 042a27a17c fix: small breakdowns fixes (#6473)
* fix: members to member, no dependents to none

* feat: add fuzzy search to multiselect options search and projects dropdown

* fix: duplicate key
2026-06-22 20:40:53 +00:00
aecsocket f6f2cb8d8b Merge branch 'main' into boris/dev-908-backend-changes 2026-06-22 19:50:47 +01:00
Prospector a747c2f290 changelog 2026-06-22 11:15:40 -07:00
Truman GaoandGitHub d756e5ab93 fix: homepage search relying on generated state (#6471) 2026-06-22 18:09:51 +00:00
Truman GaoandGitHub 1f8b1bd2bb feat: implement dependent projects + members breakdown/filter (#6395)
* feat: implement dependent project breakdown

* implement dependent project type filter

* feat: show dependent on column if there are multiple projects selected

* feat: different tooltip for when both project version and dependent breakdowns are on

* feat: implement dependents query filter

* feat: add project icon to query filter

* fix: dont show dependent on column when project breakdown is selected

* feat: add org icon

* remove: dependent on project column and project version project column

* refactor: pnpm prepr

* fix: all project selection gets unselected when reload page

* pnpm prepr

* fix: still using old search formatter

* feat: implement projects in table click to link to project page

* feat: handle analytics values that dont have a dependent project as a "No dependent" row

* pnpm prepr

* feat: remove org icon for dependents projects and change unknown label

* feat: do not include unknown rows for showing top 8

* feat: separate out no dependents based on download reason

* fix: use compatible dependencies for query filter options

* feat: implement members breakdown/filter frontend (#6470)

* feat: add link to user in table

* fix: do not allow breakdowns to occur without shared stats

* pnpm prepr

* fix: i18n and passing params for formatter
2026-06-22 18:09:00 +00:00
aecsocketandGitHub 23edb2351d Filter servers out from analytics dependents (#6445) 2026-06-22 15:07:02 +00:00
2c53e1240e feat: add analytics fetch users mapping (#6439)
* feat: add users mapping

* remove skip_serialize_if

---------

Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com>
2026-06-22 15:06:12 +00:00
aecsocketandGitHub b7d097d74d Batch incremental search event consumption (#6468)
* Batch incremental search event consumption

* fmt

* fix
2026-06-22 14:54:02 +00:00
aecsocketandGitHub 0a0ef1ce81 Fix account deletion deleting incorrect report threads (#6465) 2026-06-22 14:27:31 +00:00
d7917048d5 fix: allow PAT scope labels to wrap on long strings (#6456)
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
2026-06-21 23:03:11 +00:00
Emma F.andGitHub a0017cf58f fix: nested details tags in markdown bodies (#6458) 2026-06-21 18:56:24 +00:00
SychicandGitHub b188bbf617 fix(maven): return requested file extension (#6455)
* fix(maven): return requested file extension

closes #2428, #2663
why was this hard coded lol

* fix(maven): handle other file types properly
2026-06-21 13:09:14 +00:00
Michael H.andGitHub 79ce1adf9d chore: kebab-case topic name 2026-06-21 14:42:24 +02:00
aecsocketandGitHub c6ef10e798 Incremental search indexing (#6453)
* wip: initial kafka support

* wip: kafka search queue

* wip: kafka consumer

* wip: create/delete docs

* reindexing works

* reindex in more places

* also incrementally update server pings

* cleanup

* tombi

* fix ci

* update tombi

* prepare

* fix ci

* fix

* fix PLEASE

* 🙏

* add route to force reindex project
2026-06-21 10:03:20 +00:00
aecsocketandGitHub be65589e4d Allow v2 version creation route to accept environment (#6454)
* Allow v2 version creation route to accept environment

* fix
2026-06-20 19:30:32 +00:00
ThatGravyBoatandGitHub 86a224419c fix: users projects shifting around while in queue (#6452)
fix: users projects shifting around while in queue due to download count changes
2026-06-20 15:55:03 +00:00
Calum H.andGitHub 49dd844fe1 feat: changelog app 0.14.8 (#6450) 2026-06-19 20:34:28 +00:00
Calum H.andGitHub 56536abb24 fix: modal shift on close + readd settings preload (#6448)
* fix: NewModal layout shift on close

* fix: preload queries in settings modals

* fix: any
2026-06-19 20:23:35 +00:00
aecsocketandGitHub ac5daad280 Tiltify API query backoff (#6447)
* Tiltify API querying backoff

* fix backoff

* fix clippy

* fix2
2026-06-19 19:02:22 +00:00
checkrdandGitHub 87eec7741b fix: remove max-width cap on floating action bar for mobile screens (#6430) 2026-06-19 18:38:33 +00:00
Modrinth BotandGitHub 4c9ee5b1c4 New translations from Crowdin (main) (#6403) 2026-06-19 18:08:03 +00:00
Truman GaoandGitHub caeab46ca6 fix: clicking scrollbar closes combobox (#6443)
* fix: clicking overlay scrollbar closes combobox

* feat: adjust multiselect scrollbar style

* feat: for dropdown mutliselect too
2026-06-19 18:07:13 +00:00
Calum H.andGitHub 8e6004fdd5 fix: billing fix for medal servers (#6437)
* fix: billing fix for medal servers

* fix: lint
2026-06-19 18:04:25 +00:00
aecsocketandGitHub 50b2b9567c Fix token separators in game version search (#6435)
* Fix token separators in game version search

* fix
2026-06-19 15:50:02 +00:00
Truman GaoandGitHub e0ea14226e feat: add dawn launcher user agent (#6441)
* feat: add dawn launcher user agent

* rename to Dawn
2026-06-19 14:31:47 +00:00
Calum H.andGitHub d2b85c9f8e fix: pre-migration for skins drag and drop change (#6402) 2026-06-19 14:16:51 +00:00
aecsocketandGitHub d33f00d2b1 Tighten URL slug validation (#6442)
* Tighten URL slug validation

* slug sanitization in frontend
2026-06-19 13:52:24 +00:00
aecsocketandGitHub b3257a0614 Allow revenue analytics to bucket by user ID (#6428)
* adjust

* convert to sqlx macros

* Allow users on a team to see rev splits of other users

* prepare

* clarify comment
2026-06-18 12:12:56 +00:00
François-Xavier TalbotandGitHub 486b467af2 cont: fix(frontend): dont assume wss for panel pingtest as well (#6421)
* fix(frontend): dont assume wss for panel pingtest as well

* fix(frontend): wss assumptions

* chore: fix export

* chore: prettier
2026-06-17 22:54:09 +00:00
aecsocketandGitHub 336050f4df Clear owned projects cache when deleting an org with projects (#6429) 2026-06-17 19:24:44 +00:00
aecsocketandGitHub 2bb1ef775c Serve stale Tiltify data if API is not available (#6427) 2026-06-17 14:58:07 +00:00
François-Xavier TalbotandGitHub 5ed322d281 feat(frontend): don't assume secure protocol for kyros websocket/fs (#6420)
* feat(frontend): don't assume https as websocket/fs protocol

* fix(frontend): actually do the same for websocket

* fix(frontend): don't strip ws path
2026-06-16 21:39:38 +00:00
Calum H.andGitHub 8591bc8ebc feat: changelog app 0.14.7 (#6419) 2026-06-16 19:05:19 +00:00
01fd18d550 Fix: update modpack export filter (#6359)
* Update Modpack export filters

* fix: move file export filtering from vue to rust

* chore: remove unused import

---------

Co-authored-by: Gravy Boat <gravy@thatgravyboat.tech>
2026-06-16 18:51:45 +00:00
François-Xavier TalbotandGitHub 3aaa2ef071 feat(app-backend): key circuit breaker by URI path pattern (#6383)
* feat(app-lib): split `FetchFence` on base URI path

* chore: fmt

* fix(app-lib): fix test timing
2026-06-16 18:49:06 +00:00
Truman GaoandGitHub a3aeeac2c3 fix: babric mods incorrectly detected as fabric (#6407)
* fix: babric mods incorrectly detected as fabric

* remove: standard version, just check for fabric normalized beta version number

* add comment
2026-06-16 18:47:16 +00:00
Calum H.andGitHub dfba212be4 fix: update checking version match reliability (#6411) 2026-06-16 18:45:31 +00:00
Calum H.andGitHub 3877999e53 fix: update browse page when installing dependencies (#6414)
* fix: update browse page when installing dependencies

* fix: lint

* fix: vers page

* fix: vers page link query persistence
2026-06-16 18:36:42 +00:00
Calum H.andGitHub 46a7cf490d feat: implement dependency deletion modal for content tab (#6410)
* feat: implement dependency deletion modal for content tab

* fix: handle bulk delete state

* fix: lint
2026-06-16 18:36:30 +00:00
Truman GaoandGitHub b5f7406998 fix: game version filter in search using quotes instead of backticks (#6415) 2026-06-16 18:22:52 +00:00
ThatGravyBoatandGitHub 3679d2c786 fix: maven version ranges not matching correctly (#6416) 2026-06-16 17:28:50 +00:00
aecsocket 1a0633b6db Merge branch 'main' into boris/dev-908-backend-changes 2026-06-11 21:31:23 +01:00
aecsocket 3ae50a2abb Merge branch 'main' into boris/dev-908-backend-changes 2026-06-11 20:09:58 +01:00
tdgao 408927a23e Merge branch 'main' into boris/dev-908-backend-changes 2026-06-10 11:58:27 -07:00
tdgao 3c29a62036 fix: 2 factor auth enter code screen styles 2026-05-20 12:14:35 -06:00
aecsocket 407aa4b4cf Merge branch 'main' into boris/dev-908-backend-changes 2026-05-13 12:22:56 +01:00
aecsocket d709bbeb54 squash commits 2026-05-12 15:44:12 +01:00
Prospectorandaecsocket b5662371d8 pin tanstack versions + set pnpm min age to 7 days 2026-05-12 12:28:55 +01:00
Prospectorandaecsocket db9689b335 fix: open modrinth project links in the app (#6072) 2026-05-12 12:28:55 +01:00
Truman Gaoandaecsocket 0f1483d9df feat: improve add dependency flow (#6075)
* fix: shadow on nav

* feat: improve add dependency flow

* feat: update suggested dependency style

* feat: update dependency rows to use version number and update styles

* feat: implement combobox select searched text on focus

* feat: add Tabs.vue

* feat: update nav tabs to use tabs

* feat: improve project search dropdown

* fix: dependency search not clearing inbound query

* fix: combobox no options open state bug

* feat: improve dependency project and version search
2026-05-12 12:28:55 +01:00
Truman Gaoandaecsocket d48ad873fb fix: cannot hover over project card tooltip items (#6071)
fix: cannot hover over project cards
2026-05-12 12:28:55 +01:00
tdgao ff7c0cb5b8 fix: edit copy on info banner 2026-05-11 15:13:06 -06:00
tdgao faed8c3f6d fix: remove default date to open 13 years ago 2026-05-11 15:02:46 -06:00
tdgao 63d00cb660 fix: account.vue 2026-05-11 14:55:26 -06:00
tdgao 27d54a98af Merge branch 'main' into boris/dev-908-backend-changes 2026-05-11 14:50:39 -06:00
aecsocket 05a1a10788 fmt 2026-04-30 21:48:37 +01:00
tdgao addb6c6045 feat: add default date open view 2026-04-30 14:05:37 -06:00
aecsocket e08da23fb5 fix lints 2026-04-30 20:39:00 +01:00
tdgao ecd69f88b2 fix: hover color bad on number input 2026-04-30 11:19:56 -06:00
tdgao d834ce3645 fix: add width on date 2026-04-30 11:00:08 -06:00
tdgao cf58559381 feat: improve styles 2026-04-30 10:55:05 -06:00
tdgao 01a7ee8d1e fix: date picker icons 2026-04-30 10:28:23 -06:00
tdgao 5480826a53 fix: number input icons color 2026-04-30 10:19:01 -06:00
tdgao 9184de92df fix: calculate age based on user's timezone 2026-04-30 10:11:58 -06:00
tdgao d753b8735c fix: date picker can be null 2026-04-30 10:07:48 -06:00
aecsocket c7a5336a51 increase recursion limit 2026-04-28 23:56:47 +01:00
aecsocket 292b8f4cd2 Merge branch 'main' into boris/dev-908-backend-changes 2026-04-28 23:38:50 +01:00
aecsocket c391fb12b9 update rust toolchain 2026-04-28 23:36:43 +01:00
tdgao 62f20b9aee feat: implement preserve date for date picker 2026-04-27 14:57:20 -06:00
tdgao 2aca44bbcc feat: implement date picker component in create account 2026-04-27 14:16:32 -06:00
tdgao b9be6abf0b fix: range select border styles 2026-04-27 14:06:30 -06:00
tdgao a5be0114c5 refactor: pnpm prepr 2026-04-27 13:49:09 -06:00
tdgao f32f732c31 feat: improve UX and styles for range select 2026-04-27 13:35:32 -06:00
tdgao e974672e71 feat: implement date picker component 2026-04-27 13:18:27 -06:00
tdgao d6356ea8e5 feat: small style updates 2026-04-27 12:19:47 -06:00
tdgao 872dcbcf78 fix: authorize.vue error states 2026-04-27 12:03:43 -06:00
tdgao 44da0ed5ff refactor: convert authorize.vue to use typescript 2026-04-27 11:49:17 -06:00
tdgao ec7c893681 refactor: convert verify-email to use typescript 2026-04-27 11:14:58 -06:00
tdgao 3ce8f85463 refactor: use typescript for reset-password 2026-04-27 11:13:12 -06:00
Calum H.andGitHub 4ce82b01e6 Merge branch 'main' into boris/dev-908-backend-changes 2026-04-27 13:12:48 +01:00
tdgao f4d65437df refactor: pnpm prepr 2026-04-24 15:58:57 -06:00
tdgao 9646083025 refactor: use typescript 2026-04-24 15:55:52 -06:00
tdgao 652a9b0b5c fix: add initOnMounted for SSR race 2026-04-24 15:37:32 -06:00
tdgao 918c4a9c83 fix: last signed in style 2026-04-24 15:19:05 -06:00
tdgao 0a0b19233d Merge branch 'boris/dev-908-backend-changes' of github.com:modrinth/code into boris/dev-908-backend-changes 2026-04-24 15:10:43 -06:00
tdgao 6f2e04c3de feat: handle app create account 2026-04-24 15:10:41 -06:00
Calum H. (IMB11) ece35f9158 Revert "chore: temp staging undo"
This reverts commit cad6bd4f92.
2026-04-24 22:04:42 +01:00
Calum H. (IMB11) 3ccb8132d8 Merge branch 'boris/dev-908-backend-changes' of https://github.com/modrinth/code into boris/dev-908-backend-changes 2026-04-24 22:02:50 +01:00
Calum H. (IMB11) cad6bd4f92 chore: temp staging undo 2026-04-24 22:02:49 +01:00
tdgao 65bd8937a6 fix: hover effect on native date picker 2026-04-24 14:55:06 -06:00
tdgao abcac5f40a refactor: pnpm prepr 2026-04-24 14:46:05 -06:00
tdgao 43893158ac fix: qa issues 2026-04-24 14:43:57 -06:00
tdgao fcab720943 fix: last sign in badge color 2026-04-24 13:36:31 -06:00
tdgao 4183702a72 refactor: pnpm prepr 2026-04-24 13:02:46 -06:00
tdgao 8946e4c394 fix: append number when generated name from email is taken 2026-04-24 13:00:51 -06:00
tdgao f1ce31001a feat: implement last signed in indicator 2026-04-24 12:51:05 -06:00
tdgao c99564f8ea refactor: pnpm prepr 2026-04-24 11:38:39 -06:00
tdgao 71c1deb5cd fix: account settings modals to use new modal and fix lots of bad styles 2026-04-24 11:37:41 -06:00
tdgao 042dde075b refactor: bring root page card styles into individual components and use tailwind 2026-04-24 11:12:49 -06:00
tdgao 3bfe670d47 remove: auth/welcome page as its no longer used 2026-04-24 10:19:56 -06:00
tdgao ed53fe5330 fix: copy 2026-04-24 10:19:38 -06:00
tdgao 547921badc fix: hcaptcha styles 2026-04-24 10:12:05 -06:00
Michael H. aac61dcfaa fix: make sure staging uses staging 2026-04-24 16:26:21 +02:00
tdgao 52a972d949 refactor: pnpm prepr 2026-04-22 12:18:42 -06:00
tdgao bdc2b4abf8 Merge branch 'main' into boris/dev-908-backend-changes 2026-04-22 09:00:20 -06:00
Truman GaoandGitHub c19ce11ea4 feat: new auth flow (#5840)
* update auth with new designs

* refactor: auth.js to auth.ts

* refactor: componentize auth pages

* fix: auth pages height

* feat: initial implementation of new sign-in oauth

* fix create account flow

* fix checkbox

* remove hard coded username

* implement create user validation endpoint and add more specific error responses

* feat: implement under 13 DOB guard and email/password validation route

* fix: TOCTOU issue
2026-04-22 07:57:17 -07:00
tdgao c54906e6de Merge branch 'main' into boris/dev-908-backend-changes 2026-04-17 17:08:04 -06:00
tdgao 57b4f21080 fix: backend response for create oauth account 2026-04-17 09:45:32 -06:00
aecsocket 66f3c39c13 fix flow 2026-04-14 19:24:30 +01:00
aecsocket 7484afa18e Revert "Use user-provided callback addr instead of SELF_ADDR"
This reverts commit 7ea0635d86.
2026-04-14 19:16:14 +01:00
aecsocket 7ea0635d86 Use user-provided callback addr instead of SELF_ADDR 2026-04-14 18:20:14 +01:00
aecsocket fe3aba52ab improve URL-related OAuth code 2026-04-14 12:35:13 +01:00
aecsocket 64f87551ff fix up oauth flow routes 2026-04-13 14:19:22 +01:00
aecsocket c8a586c6f1 Backend routes for choosing username in OAuth flow 2026-04-12 21:38:04 +01:00
665 changed files with 37269 additions and 7580 deletions
@@ -81,6 +81,6 @@ Please follow these rules precisely:
Use existing patterns from our codebase:
- Variables/plurals: see `apps/frontend/src/pages/frog.vue`
- Rich-text link tags: see `apps/frontend/src/pages/auth/welcome.vue` and `apps/frontend/src/error.vue`
- Rich-text link tags: see `apps/frontend/src/error.vue`
When you finish, there should be no hard-coded English strings left in the template—everything comes from `formatMessage` or `<IntlFormatted>`.
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: taiki-e/install-action@b5fddbb5361bce8a06fb168c9d403a6cc552b084 # v2.75.29
- uses: taiki-e/install-action@fd2f5e3d644b484055ebf4268f474c565f148f25 # v2.81.9
with:
tool: tombi
- run: tombi lint
+15 -5
View File
@@ -20,6 +20,14 @@ on:
required: true
type: string
description: 'The environment to deploy to (staging-preview or production-preview)'
head_sha:
required: false
type: string
description: 'The commit SHA to deploy (defaults to github.sha)'
head_ref:
required: false
type: string
description: 'The branch name to deploy (defaults to github.ref_name)'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.environment || 'push' }}
@@ -40,12 +48,14 @@ jobs:
- name: Configure environment
id: meta
env:
ENV_INPUT: ${{ inputs.environment }}
REF: ${{ github.ref }}
DEPLOY_SHA: ${{ inputs.head_sha || github.sha }}
run: |
echo "cmd=deploy" >> $GITHUB_OUTPUT
ENV_INPUT="${{ inputs.environment }}"
REF="${{ github.ref }}"
SHA_SHORT="${GITHUB_SHA::8}"
SHA_SHORT="${DEPLOY_SHA::8}"
if [ "$ENV_INPUT" = "staging-preview" ]; then
echo "env=staging" >> $GITHUB_OUTPUT
@@ -106,8 +116,8 @@ jobs:
working-directory: ./apps/frontend
run: pnpm build
env:
CF_PAGES_BRANCH: ${{ github.ref_name }}
CF_PAGES_COMMIT_SHA: ${{ github.sha }}
CF_PAGES_BRANCH: ${{ inputs.head_ref || github.ref_name }}
CF_PAGES_COMMIT_SHA: ${{ inputs.head_sha || github.sha }}
CF_PAGES_URL: ${{ steps.meta.outputs.url }}
BUILD_ENV: ${{ steps.meta.outputs.env }}
PREVIEW: ${{ inputs.environment != '' && 'true' || 'false' }}
+9 -3
View File
@@ -24,6 +24,8 @@ jobs:
environment: [staging-preview, production-preview]
with:
environment: ${{ matrix.environment }}
head_sha: ${{ github.event.pull_request.head.sha }}
head_ref: ${{ github.event.pull_request.head.ref }}
deploy-storybook:
if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository
@@ -68,7 +70,9 @@ jobs:
- name: Configure short SHA
id: meta
run: echo "sha_short=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: echo "sha_short=${HEAD_SHA::8}" >> $GITHUB_OUTPUT
- name: Deploy Storybook preview
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
@@ -93,6 +97,8 @@ jobs:
- name: Read deployment URLs
id: urls
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
STAGING_PREVIEW_URL=$(cat deployment-url-staging-preview.txt)
PRODUCTION_PREVIEW_URL=$(cat deployment-url-production-preview.txt)
@@ -102,7 +108,7 @@ jobs:
echo "staging-preview-url=$STAGING_PREVIEW_URL" >> $GITHUB_OUTPUT
echo "production-preview-url=$PRODUCTION_PREVIEW_URL" >> $GITHUB_OUTPUT
echo "storybook-preview-url=https://git-${GITHUB_SHA::8}-storybook.modrinth.workers.dev" >> $GITHUB_OUTPUT
echo "storybook-preview-url=https://git-${HEAD_SHA::8}-storybook.modrinth.workers.dev" >> $GITHUB_OUTPUT
- name: Find comment
if: github.event_name == 'pull_request'
@@ -123,7 +129,7 @@ jobs:
body: |
## Frontend previews
Last deployed commit is [${{ github.sha }}](${{ github.event.pull_request.head.repo.html_url }}/commit/${{ github.sha }})
Last deployed commit is [${{ github.event.pull_request.head.sha }}](${{ github.event.pull_request.head.repo.html_url }}/commit/${{ github.event.pull_request.head.sha }})
| Environment | URL |
|-------------|-----|
+6
View File
@@ -99,6 +99,12 @@ jobs:
- name: Setup mold
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
- name: Install build dependencies
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
with:
packages: cmake libcurl4-openssl-dev
version: v2 # cache key
- name: Cache Cargo registry and index
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
+3 -3
View File
@@ -99,8 +99,8 @@ jobs:
- name: Install build dependencies
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
with:
packages: libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
version: v1 # cache key
packages: cmake libcurl4-openssl-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
version: v2 # cache key
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -153,7 +153,7 @@ jobs:
uses: cargo-bins/cargo-binstall@dc19f1e48450eefe5a29b8da6c6b00a87d730b37 # v1.18.1
- name: Setup nextest
run: cargo binstall --no-confirm --secure cargo-nextest@0.9.133
run: cargo binstall --no-confirm --secure --locked cargo-nextest@0.9.133
# cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall
# back to a cached cargo install
+1 -6
View File
@@ -1,12 +1,7 @@
{
"prettier.endOfLine": "lf",
"editor.formatOnSave": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
"editor.detectIndentation": false,
"editor.insertSpaces": false,
"files.eol": "\n",
+2 -2
View File
@@ -50,7 +50,7 @@ Run these from the **root** folder before opening a pull request - do not run th
- **App frontend:** `pnpm prepr:frontend:app`
- **Frontend libs:** `pnpm prepr:frontend:lib`
- **All frontend (app+web):** `pnpm prepr`
- **Labrinth (backend):** See `apps/labrinth/CLAUDE.md`
- **Labrinth (backend):** See `apps/labrinth/AGENTS.md`
The website and app `prepr` commands
@@ -62,7 +62,7 @@ The website and app `prepr` commands
## Project-Specific Instructions
Each project may have its own `CLAUDE.md` with detailed instructions:
Each project may have its own file with detailed instructions:
- [`apps/labrinth/AGENTS.md`](apps/labrinth/AGENTS.md) — Backend API
- [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website
Generated
+472 -44
View File
@@ -477,7 +477,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
"cpufeatures",
"cpufeatures 0.2.17",
"password-hash",
]
@@ -494,7 +494,7 @@ dependencies = [
"serde_json",
"thiserror 2.0.17",
"utoipa",
"uuid 1.18.1",
"uuid 1.23.3",
]
[[package]]
@@ -530,6 +530,45 @@ dependencies = [
"zbus",
]
[[package]]
name = "asn1-rs"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
dependencies = [
"asn1-rs-derive",
"asn1-rs-impl",
"displaydoc",
"nom 7.1.3",
"num-traits",
"rusticata-macros",
"thiserror 1.0.69",
"time",
]
[[package]]
name = "asn1-rs-derive"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"synstructure",
]
[[package]]
name = "asn1-rs-impl"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "astral-tokio-tar"
version = "0.5.6"
@@ -1000,7 +1039,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"tracing",
"uuid 1.18.1",
"uuid 1.23.3",
]
[[package]]
@@ -1369,6 +1408,17 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]]
name = "base64urlsafedata"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b08e33815c87d8cadcddb1e74ac307368a3751fbe40c961538afa21a1899f21c"
dependencies = [
"base64 0.21.7",
"pastey",
"serde",
]
[[package]]
name = "bindgen"
version = "0.72.1"
@@ -1831,7 +1881,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
dependencies = [
"byteorder",
"fnv",
"uuid 1.18.1",
"uuid 1.23.3",
]
[[package]]
@@ -1856,6 +1906,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
name = "chardetng"
version = "0.1.17"
@@ -2004,7 +2065,7 @@ dependencies = [
"time",
"tokio",
"url",
"uuid 1.18.1",
"uuid 1.23.3",
]
[[package]]
@@ -2332,6 +2393,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.3.0"
@@ -2706,7 +2776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
dependencies = [
"serde",
"uuid 1.18.1",
"uuid 1.23.3",
]
[[package]]
@@ -2726,6 +2796,20 @@ dependencies = [
"zeroize",
]
[[package]]
name = "der-parser"
version = "9.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
dependencies = [
"asn1-rs",
"displaydoc",
"nom 7.1.3",
"num-bigint",
"num-traits",
"rusticata-macros",
]
[[package]]
name = "deranged"
version = "0.5.4"
@@ -3871,11 +3955,25 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"r-efi 5.3.0",
"wasi 0.14.7+wasi-0.2.4",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"wasip2",
"wasip3",
]
[[package]]
name = "gif"
version = "0.13.3"
@@ -4720,6 +4818,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -4817,7 +4921,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"serde",
"serde_core",
]
@@ -5277,6 +5381,7 @@ dependencies = [
"quick-xml 0.38.3",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rdkafka",
"redis",
"regex",
"reqwest 0.12.24",
@@ -5307,8 +5412,10 @@ dependencies = [
"utoipa",
"utoipa-actix-web",
"utoipa-scalar",
"uuid 1.18.1",
"uuid 1.23.3",
"validator",
"webauthn-rs",
"webauthn-rs-proto",
"webp",
"woothee",
"yaserde",
@@ -5341,6 +5448,12 @@ dependencies = [
"spin 0.9.8",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lebe"
version = "0.5.3"
@@ -5489,6 +5602,18 @@ dependencies = [
"zlib-rs",
]
[[package]]
name = "libz-sys"
version = "1.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@@ -5742,7 +5867,7 @@ dependencies = [
"thiserror 2.0.17",
"time",
"tokio",
"uuid 1.18.1",
"uuid 1.23.3",
"wasm-bindgen-futures",
"web-sys",
"yaup",
@@ -5882,7 +6007,7 @@ dependencies = [
"rustc_version",
"smallvec",
"tagptr",
"uuid 1.18.1",
"uuid 1.23.3",
]
[[package]]
@@ -5933,7 +6058,7 @@ dependencies = [
"serde_with",
"strum",
"utoipa",
"uuid 1.18.1",
"uuid 1.23.3",
]
[[package]]
@@ -6568,6 +6693,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "oid-registry"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
dependencies = [
"asn1-rs",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -6598,15 +6732,14 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.73"
version = "0.10.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"
dependencies = [
"bitflags 2.9.4",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
@@ -6630,9 +6763,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.109"
version = "0.9.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
dependencies = [
"cc",
"libc",
@@ -6936,6 +7069,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pastey"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
name = "path-util"
version = "0.0.0"
@@ -7764,6 +7903,12 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "r2d2"
version = "0.8.10"
@@ -7816,6 +7961,17 @@ dependencies = [
"rand_core 0.9.3",
]
[[package]]
name = "rand"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20",
"getrandom 0.4.2",
"rand_core 0.10.1",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
@@ -7873,6 +8029,12 @@ dependencies = [
"getrandom 0.3.3",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_hc"
version = "0.2.0"
@@ -7967,6 +8129,37 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "rdkafka"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1beea247b9a7600a81d4cc33f659ce1a77e1988323d7d2809c7ed1c21f4c316d"
dependencies = [
"futures-channel",
"futures-util",
"libc",
"log",
"rdkafka-sys",
"serde",
"serde_derive",
"serde_json",
"slab",
"tokio",
]
[[package]]
name = "rdkafka-sys"
version = "4.10.0+2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e234cf318915c1059d4921ef7f75616b5219b10b46e9f3a511a15eb4b56a3f77"
dependencies = [
"cmake",
"libc",
"libz-sys",
"num_enum",
"pkg-config",
]
[[package]]
name = "redis"
version = "0.32.7"
@@ -8269,7 +8462,7 @@ dependencies = [
"rkyv_derive",
"seahash",
"tinyvec",
"uuid 1.18.1",
"uuid 1.23.3",
]
[[package]]
@@ -8408,6 +8601,15 @@ dependencies = [
"semver",
]
[[package]]
name = "rusticata-macros"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
dependencies = [
"nom 7.1.3",
]
[[package]]
name = "rustix"
version = "0.38.44"
@@ -8652,7 +8854,7 @@ dependencies = [
"serde",
"serde_json",
"url",
"uuid 1.18.1",
"uuid 1.23.3",
]
[[package]]
@@ -8927,7 +9129,7 @@ dependencies = [
"thiserror 2.0.17",
"time",
"url",
"uuid 1.18.1",
"uuid 1.23.3",
]
[[package]]
@@ -8984,6 +9186,16 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_cbor_2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aec2709de9078e077090abd848e967abab63c9fb3fdb5d4799ad359d8d482c"
dependencies = [
"half 2.7.0",
"serde",
]
[[package]]
name = "serde_core"
version = "1.0.228"
@@ -9230,7 +9442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -9247,7 +9459,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -9503,7 +9715,7 @@ dependencies = [
"tokio-stream",
"tracing",
"url",
"uuid 1.18.1",
"uuid 1.23.3",
"webpki-roots 0.26.11",
]
@@ -9586,7 +9798,7 @@ dependencies = [
"stringprep",
"thiserror 2.0.17",
"tracing",
"uuid 1.18.1",
"uuid 1.23.3",
"whoami",
]
@@ -9626,7 +9838,7 @@ dependencies = [
"stringprep",
"thiserror 2.0.17",
"tracing",
"uuid 1.18.1",
"uuid 1.23.3",
"whoami",
]
@@ -9653,7 +9865,7 @@ dependencies = [
"thiserror 2.0.17",
"tracing",
"url",
"uuid 1.18.1",
"uuid 1.23.3",
]
[[package]]
@@ -10118,7 +10330,7 @@ dependencies = [
"thiserror 2.0.17",
"time",
"url",
"uuid 1.18.1",
"uuid 1.23.3",
"walkdir",
]
@@ -10426,7 +10638,7 @@ dependencies = [
"toml 0.9.8",
"url",
"urlpattern",
"uuid 1.18.1",
"uuid 1.23.3",
"walkdir",
]
@@ -10582,7 +10794,7 @@ dependencies = [
"tracing-error",
"tracing-subscriber",
"url",
"uuid 1.18.1",
"uuid 1.23.3",
"whoami",
"windows",
"windows-core 0.61.2",
@@ -10630,7 +10842,7 @@ dependencies = [
"tracing-error",
"url",
"urlencoding",
"uuid 1.18.1",
"uuid 1.23.3",
"webview2-com",
"windows",
"windows-core 0.61.2",
@@ -11119,9 +11331,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.41"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
@@ -11139,14 +11351,14 @@ dependencies = [
"mutually_exclusive_features",
"pin-project",
"tracing",
"uuid 1.18.1",
"uuid 1.23.3",
]
[[package]]
name = "tracing-attributes"
version = "0.1.30"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
@@ -11155,9 +11367,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.34"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
@@ -11553,7 +11765,7 @@ dependencies = [
"regex",
"syn 2.0.106",
"url",
"uuid 1.18.1",
"uuid 1.23.3",
]
[[package]]
@@ -11579,14 +11791,14 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.18.1"
version = "1.23.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
dependencies = [
"getrandom 0.3.3",
"getrandom 0.4.2",
"js-sys",
"rand 0.9.2",
"serde",
"rand 0.10.1",
"serde_core",
"wasm-bindgen",
]
@@ -11761,7 +11973,16 @@ version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
"wit-bindgen 0.46.0",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen 0.51.0",
]
[[package]]
@@ -11829,6 +12050,28 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap 2.11.4",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasm-streams"
version = "0.4.2"
@@ -11855,6 +12098,18 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags 2.9.4",
"hashbrown 0.15.5",
"indexmap 2.11.4",
"semver",
]
[[package]]
name = "wayland-backend"
version = "0.3.11"
@@ -11935,6 +12190,74 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webauthn-attestation-ca"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6475c0bbd1a3f04afaa3e98880408c5be61680c5e6bd3c6f8c250990d5d3e18e"
dependencies = [
"base64urlsafedata",
"openssl",
"openssl-sys",
"serde",
"tracing",
"uuid 1.23.3",
]
[[package]]
name = "webauthn-rs"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c548915e0e92ee946bbf2aecf01ea21bef53d974b0793cc6732ba81a03fc422"
dependencies = [
"base64urlsafedata",
"serde",
"tracing",
"url",
"uuid 1.23.3",
"webauthn-rs-core",
]
[[package]]
name = "webauthn-rs-core"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "296d2d501feb715d80b8e186fb88bab1073bca17f460303a1013d17b673bea6a"
dependencies = [
"base64 0.21.7",
"base64urlsafedata",
"der-parser",
"hex",
"nom 7.1.3",
"openssl",
"openssl-sys",
"rand 0.9.2",
"rand_chacha 0.9.0",
"serde",
"serde_cbor_2",
"serde_json",
"thiserror 1.0.69",
"tracing",
"url",
"uuid 1.23.3",
"webauthn-attestation-ca",
"webauthn-rs-proto",
"x509-parser",
]
[[package]]
name = "webauthn-rs-proto"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c37393beac9c1ed1ca6dbb30b1e01783fb316ab3a45d90ecd48c99052dd7ef1e"
dependencies = [
"base64 0.21.7",
"base64urlsafedata",
"serde",
"serde_json",
"url",
]
[[package]]
name = "webkit2gtk"
version = "2.0.2"
@@ -12657,6 +12980,94 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck 0.5.0",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck 0.5.0",
"indexmap 2.11.4",
"prettyplease",
"syn 2.0.106",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn 2.0.106",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags 2.9.4",
"indexmap 2.11.4",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap 2.11.4",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "woothee"
version = "0.13.0"
@@ -12748,6 +13159,23 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "x509-parser"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
dependencies = [
"asn1-rs",
"data-encoding",
"der-parser",
"lazy_static",
"nom 7.1.3",
"oid-registry",
"rusticata-macros",
"thiserror 1.0.69",
"time",
]
[[package]]
name = "xattr"
version = "1.6.1"
+4
View File
@@ -140,6 +140,7 @@ quick-xml = "0.38.3"
quote = { version = "1.0" }
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
rdkafka = { version = "0.36.2", features = ["cmake-build"] }
redis = "0.32.7"
regex = "1.12.2"
reqwest = { version = "0.12.24", default-features = false }
@@ -219,6 +220,8 @@ utoipa-actix-web = { version = "0.1.2" }
utoipa-scalar = { version = "0.3.0", default-features = false }
uuid = "1.18.1"
validator = "0.20.0"
webauthn-rs = "0.5.5"
webauthn-rs-proto = "0.5.5"
webp = { version = "0.3.1", default-features = false }
webview2-com = "0.38.0" # Should be updated in lockstep with wry
whoami = "1.6.1"
@@ -269,6 +272,7 @@ redundant_type_annotations = "warn"
result_large_err = "allow"
todo = "warn"
too_many_arguments = "allow"
type_complexity = "allow"
uninlined_format_args = "warn"
unnested_or_patterns = "warn"
wildcard_dependencies = "warn"
@@ -89,14 +89,9 @@ const initFiles = async () => {
disabled:
folder === 'profile.json' ||
folder.startsWith('modrinth_logs') ||
folder.startsWith('.fabric'),
folder.startsWith('.fabric') ||
folder.startsWith('__MACOSX'),
}))
.filter(
(pathData) =>
!pathData.path.includes('.DS_Store') &&
pathData.path !== 'mods/.connector' &&
!pathData.path.startsWith('mods/.connector/'),
)
.forEach((pathData) => {
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
if (parent !== '') {
@@ -13,7 +13,7 @@ import {
} from '@modrinth/ui'
import type { GameVersionTag, PlatformTag } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, shallowRef } from 'vue'
import { computed, ref } from 'vue'
import { trackEvent } from '@/helpers/analytics'
import { get_project_versions, get_version } from '@/helpers/cache'
@@ -46,55 +46,62 @@ debug('metadata load: start', {
installStage: instance.value.install_stage,
})
const [
fabric_versions,
forge_versions,
quilt_versions,
neoforge_versions,
all_game_versions,
loaders,
] = await Promise.all([
get_loader_versions('fabric')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_loader_versions('forge')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_loader_versions('quilt')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_loader_versions('neo')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_game_versions()
.then((gameVersions: GameVersionTag[]) => shallowRef(gameVersions))
.catch(handleError),
get_loaders()
.then((value: PlatformTag[]) =>
value
.filter(
(item) => item.supported_project_types.includes('modpack') || item.name === 'vanilla',
)
.sort((a, b) => (a.name === 'vanilla' ? -1 : b.name === 'vanilla' ? 1 : 0)),
)
.then((loader: PlatformTag[]) => ref(loader))
.catch(handleError),
])
function getSupportedModpackLoaders() {
return get_loaders().then((value: PlatformTag[]) =>
value
.filter((item) => item.supported_project_types.includes('modpack') || item.name === 'vanilla')
.sort((a, b) => (a.name === 'vanilla' ? -1 : b.name === 'vanilla' ? 1 : 0)),
)
}
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 fabricVersionsQuery = useQuery({
queryKey: ['instance-settings', 'loader-versions', 'fabric'],
queryFn: () => get_loader_versions('fabric') as Promise<Manifest>,
})
const forgeVersionsQuery = useQuery({
queryKey: ['instance-settings', 'loader-versions', 'forge'],
queryFn: () => get_loader_versions('forge') as Promise<Manifest>,
})
const quiltVersionsQuery = useQuery({
queryKey: ['instance-settings', 'loader-versions', 'quilt'],
queryFn: () => get_loader_versions('quilt') as Promise<Manifest>,
})
const neoforgeVersionsQuery = useQuery({
queryKey: ['instance-settings', 'loader-versions', 'neo'],
queryFn: () => get_loader_versions('neo') as Promise<Manifest>,
})
const gameVersionsQuery = useQuery({
queryKey: ['instance-settings', 'game-versions'],
queryFn: () => get_game_versions() as Promise<GameVersionTag[]>,
})
const loadersQuery = useQuery({
queryKey: ['instance-settings', 'loaders', 'modpack'],
queryFn: getSupportedModpackLoaders,
})
const { data: modpackInfo } = useQuery({
const metadataLoading = computed(() =>
[
fabricVersionsQuery,
forgeVersionsQuery,
quiltVersionsQuery,
neoforgeVersionsQuery,
gameVersionsQuery,
loadersQuery,
].some((query) => query.isLoading.value),
)
debug('metadata queries configured', {
instancePath: instance.value.path,
loader: instance.value.loader,
gameVersion: instance.value.game_version,
})
const modpackInfoQuery = useQuery({
queryKey: computed(() => ['linkedModpackInfo', instance.value.path]),
queryFn: () => get_linked_modpack_info(instance.value.path, 'must_revalidate'),
enabled: computed(() => !!instance.value.linked_data?.project_id && !offline),
})
const modpackInfo = modpackInfoQuery.data
const repairing = ref(false)
const reinstalling = ref(false)
@@ -107,17 +114,17 @@ const messages = defineMessages({
})
function getManifest(loader: string) {
const map: Record<string, typeof fabric_versions> = {
fabric: fabric_versions,
forge: forge_versions,
quilt: quilt_versions,
neoforge: neoforge_versions,
const map: Record<string, Manifest | undefined> = {
fabric: fabricVersionsQuery.data.value,
forge: forgeVersionsQuery.data.value,
quilt: quiltVersionsQuery.data.value,
neoforge: neoforgeVersionsQuery.data.value,
}
const manifest = map[loader]
debug('getManifest:', {
loader,
hasManifest: !!manifest?.value,
gameVersions: manifest?.value?.gameVersions?.length ?? 0,
hasManifest: !!manifest,
gameVersions: manifest?.gameVersions?.length ?? 0,
})
return manifest
}
@@ -143,7 +150,7 @@ provideAppBackup({
provideInstallationSettings({
closeSettings: closeModal,
loading: ref(false),
loading: computed(() => metadataLoading.value || modpackInfoQuery.isLoading.value),
installationInfo: computed(() => {
const rows = [
{
@@ -185,14 +192,14 @@ provideInstallationSettings({
currentPlatform: computed(() => instance.value.loader),
currentGameVersion: computed(() => instance.value.game_version),
currentLoaderVersion: computed(() => instance.value.loader_version ?? ''),
availablePlatforms: loaders?.value?.map((x) => x.name) ?? [],
availablePlatforms: computed(() => loadersQuery.data.value?.map((x) => x.name) ?? []),
resolveGameVersions(loader, showSnapshots) {
const versions = all_game_versions?.value ?? []
const versions = gameVersionsQuery.data.value ?? []
const filtered = versions.filter((item) => {
if (loader === 'vanilla') return true
const manifest = getManifest(loader)
return !!manifest?.value?.gameVersions?.some((x) => item.version === x.id)
return !!manifest?.gameVersions?.some((x) => item.version === x.id)
})
const result = (
showSnapshots ? filtered : filtered.filter((x) => x.version_type === 'release')
@@ -213,12 +220,12 @@ provideInstallationSettings({
return []
}
const manifest = getManifest(loader)
if (!manifest?.value) {
if (!manifest) {
debug('resolveLoaderVersions: no manifest', { loader, gameVersion })
return []
}
if (loader === 'fabric' || loader === 'quilt') {
const result = manifest.value.gameVersions[0]?.loaders ?? []
const result = manifest.gameVersions[0]?.loaders ?? []
debug('resolveLoaderVersions: fabric/quilt result', {
loader,
gameVersion,
@@ -226,14 +233,13 @@ provideInstallationSettings({
})
return result
}
const result =
manifest.value.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? []
const result = manifest.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 ?? []
const versions = gameVersionsQuery.data.value ?? []
if (loader === 'vanilla') {
const result = versions.some((x) => x.version_type !== 'release')
debug('resolveHasSnapshots: vanilla', { loader, result })
@@ -241,7 +247,7 @@ provideInstallationSettings({
}
const manifest = getManifest(loader)
const supported = versions.filter(
(item) => !!manifest?.value?.gameVersions?.some((x) => item.version === x.id),
(item) => !!manifest?.gameVersions?.some((x) => item.version === x.id),
)
const result = supported.some((x) => x.version_type !== 'release')
debug('resolveHasSnapshots:', {
@@ -16,7 +16,8 @@ import {
type TabbedModalTab,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import type { PlatformTag } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import { computed, nextTick, ref, watch } from 'vue'
@@ -26,7 +27,9 @@ import InstallationSettings from '@/components/ui/instance_settings/Installation
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
import { get_project_v3 } from '@/helpers/cache'
import { get_loader_versions } from '@/helpers/metadata'
import { get_linked_modpack_info } from '@/helpers/profile'
import { get_game_versions, get_loaders } from '@/helpers/tags'
import { provideInstanceSettings } from '@/providers/instance-settings'
import type { GameInstance } from '../../../helpers/types'
@@ -45,7 +48,6 @@ const isMinecraftServer = ref(false)
const handleUnlinked = () => emit('unlinked')
const instanceRef = computed(() => props.instance)
const queryClient = useQueryClient()
const tabbedModal = ref<InstanceType<typeof TabbedModal> | null>(null)
function hide() {
@@ -120,13 +122,46 @@ const tabs = computed<TabbedModalTab[]>(() => [
},
])
function getSupportedModpackLoaders() {
return get_loaders().then((value: PlatformTag[]) =>
value
.filter((item) => item.supported_project_types.includes('modpack') || item.name === 'vanilla')
.sort((a, b) => (a.name === 'vanilla' ? -1 : b.name === 'vanilla' ? 1 : 0)),
)
}
// Preload
useQuery({
queryKey: ['instance-settings', 'loader-versions', 'fabric'],
queryFn: () => get_loader_versions('fabric'),
})
useQuery({
queryKey: ['instance-settings', 'loader-versions', 'forge'],
queryFn: () => get_loader_versions('forge'),
})
useQuery({
queryKey: ['instance-settings', 'loader-versions', 'quilt'],
queryFn: () => get_loader_versions('quilt'),
})
useQuery({
queryKey: ['instance-settings', 'loader-versions', 'neo'],
queryFn: () => get_loader_versions('neo'),
})
useQuery({
queryKey: ['instance-settings', 'game-versions'],
queryFn: get_game_versions,
})
useQuery({
queryKey: ['instance-settings', 'loaders', 'modpack'],
queryFn: getSupportedModpackLoaders,
})
useQuery({
queryKey: computed(() => ['linkedModpackInfo', props.instance.path]),
queryFn: () => get_linked_modpack_info(props.instance.path, 'stale_while_revalidate'),
enabled: computed(() => !!props.instance.linked_data?.project_id && !props.offline),
})
function show(tabIndex?: number) {
if (props.instance.linked_data?.project_id) {
queryClient.prefetchQuery({
queryKey: ['linkedModpackInfo', props.instance.path],
queryFn: () => get_linked_modpack_info(props.instance.path, 'stale_while_revalidate'),
})
}
tabbedModal.value?.show()
if (tabIndex !== undefined) {
nextTick(() => tabbedModal.value?.setTab(tabIndex))
+135 -9
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "جاز تنزيل إصدار جافا {version}"
},
"app.action-bar.downloading-update": {
"message": "تحميل التحديث"
},
"app.action-bar.downloads": {
"message": "التنزيلات"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "النسخة ألرئيسية"
},
"app.action-bar.reload-to-update": {
"message": "أعد التشغيل للتحديث"
},
"app.action-bar.show-more-running-instances": {
"message": "عرض المزيد من النسخ قيد التشغيل"
},
"app.action-bar.stop-instance": {
"message": "إيقاف النسخة"
},
"app.action-bar.update": {
"message": "تحديث"
},
"app.action-bar.view-active-downloads": {
"message": "عرض التحميلات الفعالة"
},
@@ -35,12 +44,36 @@
"app.action-bar.view-logs": {
"message": "عرض التسجيل"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "يتيح العرض المتقدم لتأثيرات مثل التشويش الذي قد تسبب مشكلات في الأداء دون استخدام العرض المسرع بالأجهزة."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "العرض المتقدم"
},
"app.appearance-settings.color-theme.description": {
"message": "اختر لون سمتك المفضل من أجل تطبيق Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "لون السمة"
},
"app.appearance-settings.default-landing-page.description": {
"message": "غير الصفحة التي يفتح عليها المشغل."
},
"app.appearance-settings.default-landing-page.home": {
"message": "صفحة الرئيسية"
},
"app.appearance-settings.default-landing-page.library": {
"message": "مكتبة"
},
"app.appearance-settings.default-landing-page.title": {
"message": "الصفحة الرئيسة الافتراضية"
},
"app.appearance-settings.hide-nametag.description": {
"message": "ألغ تفعيل عرض الاسم فوق لاعبك في صفحة المظاهر."
},
"app.appearance-settings.hide-nametag.title": {
"message": "إخفاء عرض الاسم"
},
"app.auth-servers.unreachable.body": {
"message": "قد تكون خوادم مصادقة ماينكرافت معطلة حاليًا. تحقق من اتصالك بالإنترنت وحاول مرة أخرى لاحقًا."
},
@@ -59,15 +92,27 @@
"app.browse.already-added": {
"message": "مضاف فعلا"
},
"app.browse.back-to-instance": {
"message": "العودة للنموذج"
},
"app.browse.discover-content": {
"message": "استكشف محتوى"
},
"app.browse.discover-servers": {
"message": "استكشف خوادم"
},
"app.browse.hide-added-servers": {
"message": "إخفاء الخوادم المضافة مسبقًا"
},
"app.browse.server.installing": {
"message": "جاري التثبيت"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "تنزيل الحزم"
},
"app.export-modal.description-placeholder": {
"message": "أدخل وصف التعديل..."
},
@@ -83,6 +128,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "إسم حزمة التعديل"
},
"app.export-modal.select-files-label": {
"message": "أختيار الملفات التي سيتم تصديرها"
},
"app.export-modal.version-number-label": {
"message": "رقم الإصدار"
},
@@ -102,13 +150,13 @@
"message": "حذف النموذج"
},
"app.instance.modpack-already-installed.body": {
"message": "حُزْمَة التعديل هذه مثبته فعلًا في نموذج <bold>{instanceName}</bold>. هل انت متأكد بإرادة نسخه؟"
"message": "حُزْمَة التعديل مثبته مسبقًا فعلًا في نموذج <bold>{instanceName}</bold>. هل أنت متأكد بإرادة نسخه؟"
},
"app.instance.modpack-already-installed.create": {
"message": "إنشاء"
},
"app.instance.modpack-already-installed.header": {
"message": "حُزْمَة التعديل مثبتة بالفعل"
"message": "حُزْمَة التعديل مثبتة فعلًا"
},
"app.instance.modpack-already-installed.instance": {
"message": "النموذج"
@@ -126,7 +174,7 @@
"message": "تحقق من المشاريع التي أستخدمها في حزمة التعديل الخاص بي!"
},
"app.instance.mods.share-title": {
"message": "مشاركة محتوى حزمة التعديل"
"message": "مشاركة محتوى حُزْمَة التعديل"
},
"app.instance.mods.successfully-uploaded": {
"message": "تم الرفع بنجاح"
@@ -189,7 +237,7 @@
"message": "{count, plural, one {# تعديل} other {# تعديلات}}"
},
"app.modal.install-to-play.required-modpack": {
"message": زمة التعديل مطلوبة"
"message": ُزْمَة التعديل مطلوبة"
},
"app.modal.install-to-play.server-requires-mods": {
"message": "يتطلب هذا الخادم تعديلات للعب. انقر فوق \"تثبيت\" لإعداد الملفات المطلوبة من Modrinth، ثم قم بتشغيله مباشرة إلى الخادم."
@@ -210,7 +258,13 @@
"message": "يلزم التحديث"
},
"app.modal.update-to-play.update-required-description": {
"message": "هناك تحديث مطلوب للعب بـ {name}. الرجاء التحديث إلى آخر اصدار لتشغيل اللعبة."
"message": "هناك تحديث مطلوب للعب بـ {name}. الرجاء التحديث إلى أحدث اصدار لتشغيل اللعبة."
},
"app.project.install-context.back-to-browse": {
"message": "العودة إلى الاستكشاف"
},
"app.project.install-context.install-content-to-instance": {
"message": "تثبيت المحتويات الى النموذج"
},
"app.settings.developer-mode-enabled": {
"message": "تم تفعيل وضع المطوّر."
@@ -236,6 +290,75 @@
"app.settings.tabs.resource-management": {
"message": "إدارة الموارد"
},
"app.skins.add-button": {
"message": "إضافة مظهر"
},
"app.skins.add-button.drag-and-drop": {
"message": "أسحب ثم أفلت"
},
"app.skins.apply-button": {
"message": "تطبيق"
},
"app.skins.delete-button": {
"message": "حذف المظهر"
},
"app.skins.delete-modal.description": {
"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.make-edit-first-tooltip": {
"message": "عدل المظهر أولا للحفظ !"
},
"app.skins.modal.no-cape-tooltip": {
"message": "بدون رداء"
},
"app.skins.modal.none-cape-option": {
"message": "لا شيء"
},
"app.skins.modal.save-skin-button": {
"message": "حفظ المظهر"
},
"app.skins.modal.saving-tooltip": {
"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.sign-in.title": {
"message": "الرجاء تسجيل الدخول"
},
"app.skins.title": {
"message": "اختيار المظهر"
},
"app.update-popup.body.download-complete": {
"message": "انتهى تنزيل تطبيق Modrinth v{version}. أعد التحميل للتحديث الآن، أو تلقائيًا عند إغلاق تطبيق Modrinth."
},
@@ -254,9 +377,6 @@
"app.update-popup.download-complete": {
"message": "اكتمل التنزيل"
},
"app.update-popup.reload": {
"message": "إعادة تحميل"
},
"app.update-popup.title": {
"message": "تحديث متاح"
},
@@ -267,7 +387,7 @@
"message": "تم تثبيت الإصدار {version} بنجاح!"
},
"app.world.server-modal.placeholder-address": {
"message": "مثال.مودرنث.جج"
"message": "مثال (example.modrinth.gg)"
},
"app.world.server-modal.select-an-option": {
"message": "حدد خيارا"
@@ -518,6 +638,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "الذاكرة المخصَّصة"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "النافذة"
},
@@ -593,6 +716,9 @@
"minecraft-account.add-account": {
"message": "إضافة حساب"
},
"minecraft-account.label": {
"message": "حساب ماينكرافت"
},
"minecraft-account.select-account": {
"message": "اختيار الحساب"
},
+294 -108
View File
@@ -2,14 +2,17 @@
"app.action-bar.downloading-java": {
"message": "Stahování Javy {version}"
},
"app.action-bar.downloading-update": {
"message": "Stahování aktualizace"
},
"app.action-bar.downloads": {
"message": "Stažené Soubory"
"message": "Stahování"
},
"app.action-bar.hide-more-running-instances": {
"message": "Skrýt více spuštěných instancí"
},
"app.action-bar.make-primary-instance": {
"message": "Použít jako primární instanci"
"message": "Nastavit jako primární instanci"
},
"app.action-bar.no-instances-running": {
"message": "Žádné spuštěné instance"
@@ -20,11 +23,17 @@
"app.action-bar.primary-instance": {
"message": "Primární instance"
},
"app.action-bar.reload-to-update": {
"message": "Znovu načíst pro aktualizaci"
},
"app.action-bar.show-more-running-instances": {
"message": "Zobrazit další spuštěné instance"
},
"app.action-bar.stop-instance": {
"message": "Ukončit instanci"
"message": "Zastavit instanci"
},
"app.action-bar.update": {
"message": "Aktualizace"
},
"app.action-bar.view-active-downloads": {
"message": "Zobrazit probíhající stahování"
@@ -33,10 +42,10 @@
"message": "Zobrazit instanci"
},
"app.action-bar.view-logs": {
"message": "Zobrazit záznamy"
"message": "Zobrazit logy"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Umožňuje pokročilé vykreslování, jako jsou efekty rozostření, které mohou způsobit problémy s výkonem bez hardwarového vykreslení."
"message": "Povolí pokročilé vykreslování, například efekty rozmazání, které mohou bez hardwarového zrychlení způsobovat problémy s výkonem."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Pokročilé vykreslování"
@@ -48,16 +57,16 @@
"message": "Barevný motiv"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Změňte stránku, na které se spouštěč otevře."
"message": "Změňte stránku, která se otevře po spuštění launcheru."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Domov"
"message": "Domů"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Knihovna"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Výchozí úvodní stránka"
"message": "Výchozí stránka po spuštění"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Zakáže zobrazení jmenovky nad vaší postavou na stránce skinů."
@@ -66,31 +75,34 @@
"message": "Skrýt jmenovku"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "V sekci „Vrátit se do hry“ na úvodní stránce jsou uvedeny nejnovější světy."
"message": "Zahrne nedávné světy do sekce „Vrátit se do hry“ na domovské stránce."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Skočit zpět do světů"
"message": "Rychlý návrat do světů"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimalizovat spouštěč při spuštění procesu Minecraftu."
"message": "Minimalizovat launcher při spuštění Minecraftu."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimalizovat spouštěč"
"message": "Minimalizovat launcher"
},
"app.appearance-settings.native-decorations.description": {
"message": "Použít systémový rámeček okna (vyžaduje restart aplikace)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Nativní dekorace"
"message": "Systémové dekorace"
},
"app.appearance-settings.select-option": {
"message": "Vyberte možnost"
},
"app.appearance-settings.show-play-time.description": {
"message": "Zobrazuje, kolik času jsi strávil hraním instance."
},
"app.appearance-settings.show-play-time.title": {
"message": "Ukázat herní čas"
"message": "Zobrazit odehraný čas"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Umožňí přepínat zobrazení postranního panelu."
"message": "Umožní přepínat zobrazení postranního panelu."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Přepnout postranní panel"
@@ -102,19 +114,19 @@
"message": "Upozorni mě před instalací neznámých modpacků"
},
"app.auth-servers.unreachable.body": {
"message": "Autořizační servery Minecraftu aktuálně nejsou k dispozici. Zkontrolujte si své připojení k internetu a zkuste to znovu později."
"message": "Ověřovací Minecraft servery mohou být právě teď nedostupné. Zkontroluj si své připojení k internetu a zkus to znovu později."
},
"app.auth-servers.unreachable.header": {
"message": "Připojení k autorizačním serverům se nezdařilo"
"message": "Nelze se připojit k ověřovacím serverům"
},
"app.browse.add-servers-to-instance": {
"message": "Přidat server do instance"
"message": "Přidání serveru do instance"
},
"app.browse.add-to-an-instance": {
"message": "Přidat do instalace"
"message": "Přidat do instance"
},
"app.browse.add-to-instance": {
"message": "Přidat do instalace"
"message": "Přidat do instance"
},
"app.browse.add-to-instance-name": {
"message": "Přidat do {instanceName}"
@@ -123,22 +135,22 @@
"message": "Přidáno"
},
"app.browse.already-added": {
"message": "Už přidáno"
"message": "Již přidáno"
},
"app.browse.back-to-instance": {
"message": "Zpět k instanci"
},
"app.browse.discover-content": {
"message": "Prozkoumat obsah"
"message": "Objevovat obsah"
},
"app.browse.discover-servers": {
"message": "Prozkoumat servery"
"message": "Objevovat servery"
},
"app.browse.hide-added-servers": {
"message": "Skrýt již přidané servery"
},
"app.browse.project-type.modpacks": {
"message": "Balíčky Modifikací"
"message": "Modpacky"
},
"app.browse.server-instance-content-warning": {
"message": "Přidání obsahu může narušit kompatibilitu při připojení k serveru. Veškerý přidaný obsah se navíc ztratí při aktualizaci obsahu instance serveru."
@@ -146,6 +158,9 @@
"app.browse.server.installing": {
"message": "Instalování"
},
"app.content-install.no-compatible-versions": {
"message": "Žádná dostupná verze se neshoduje s verzí {compatibilityLabel}. Vyber verzi, abys ji i přesto nainstaloval/a. Závislosti nebudou automaticky nainstalovány."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -153,7 +168,7 @@
"message": "Probíhá instalace modpacku..."
},
"app.export-modal.description-placeholder": {
"message": "Přidej popis modpacku..."
"message": "Zadej popis modpacku..."
},
"app.export-modal.export-button": {
"message": "Exportovat"
@@ -165,10 +180,10 @@
"message": "Zahrnout \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Jméno Modpacku"
"message": "Název modpacku"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Jméno modpacku"
"message": "Název modpacku"
},
"app.export-modal.select-files-label": {
"message": "Nastavte, které soubory mají být do tohoto exportu zahrnuty"
@@ -180,7 +195,7 @@
"message": "1.0.0"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Všechna data z instance budou navždy smazána, včetně světů, nastavení a všeho ostatního."
"message": "Všechna data tvé instance budou trvale smazána, včetně světů, konfigurací a veškerého nainstalovaného obsahu."
},
"app.instance.confirm-delete.admonition-header": {
"message": "Tato akce nemůže být vrácena"
@@ -189,19 +204,19 @@
"message": "Odstranit instanci"
},
"app.instance.confirm-delete.header": {
"message": "Odstanit instanci"
"message": "Odstranění instance"
},
"app.instance.modpack-already-installed.body": {
"message": "Tento modpack je už nainstalovaný v instanci <bold>{instanceName}</bold>. Opravdu ho chceš duplikovat?"
"message": "Tento modpack je již nainstalovaný v instanci <bold>{instanceName}</bold>. Opravdu ho chceš duplikovat?"
},
"app.instance.modpack-already-installed.create": {
"message": "Vytvořit"
},
"app.instance.modpack-already-installed.header": {
"message": "Modpack už je nainstalovaný"
"message": "Modpack je již nainstalován"
},
"app.instance.modpack-already-installed.instance": {
"message": "Instalace"
"message": "Instance"
},
"app.instance.mods.content-type-project": {
"message": "projekt"
@@ -213,7 +228,7 @@
"message": "{count} projektů bylo přidáno"
},
"app.instance.mods.share-text": {
"message": "Podívejte se na projekty co používám ve svém modpacku!"
"message": "Podívejte se na projekty, které používám ve svém modpacku!"
},
"app.instance.mods.share-title": {
"message": "Sdílení obsahu modpacku"
@@ -227,11 +242,14 @@
"app.instance.worlds.browse-servers": {
"message": "Procházet servery"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' bude **navždy smazán** a nebude možné jej obnovit."
},
"app.instance.worlds.delete-world-title": {
"message": "Opravdu si jsi jistý, že chceš navždy smazat tenhle svět?"
"message": "Jsi si jistý, že chceš trvale smazat tento svět?"
},
"app.instance.worlds.filter-modded": {
"message": "Módováno"
"message": "Módovaný"
},
"app.instance.worlds.filter-offline": {
"message": "Offline"
@@ -243,19 +261,22 @@
"message": "Vanilla"
},
"app.instance.worlds.no-worlds-description": {
"message": "Přidej nebo procházej servery"
"message": "Přidej server nebo začni procházet"
},
"app.instance.worlds.no-worlds-heading": {
"message": "Žádné servery nebo světy nebyly přidány"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' bude odstraněn z tvého seznamu i ze hry a nebude možné jej obnovit."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) bude odstraněn z vašeho seznamu, a to i ve hře, a nebude možné jej obnovit."
"message": "'{name}' ({address}) bude odstraněn z tvého seznamu i ze hry a nebude možné jej obnovit."
},
"app.instance.worlds.remove-server-title": {
"message": "Opravdu chceš odstranit {name}?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Hledat ve světech {count}..."
"message": "Hledat v {count} světech..."
},
"app.instance.worlds.this-server": {
"message": "tento server"
@@ -264,19 +285,19 @@
"message": "Požadovaný obsah"
},
"app.modal.install-to-play.header": {
"message": "Nainstaluj ke hraní"
"message": "Nainstaluj pro hraní"
},
"app.modal.install-to-play.install-button": {
"message": "Instalovat"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural,one {#mód} other {#módy}}"
"message": "{count, plural, one {# mód} few {# módy} other {# módů}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Povinný modpack"
"message": "Požadovaný modpack"
},
"app.modal.install-to-play.server-requires-mods": {
"message": "Tento server k hraní vyžaduje módy. Klikni na instalovat pro získání potřebných módů z Modrinth."
"message": "Tento server k hraní vyžaduje módy. Klikni na instalovat pro získání potřebných módů z Modrinth a poté se rovnou připoj na server."
},
"app.modal.install-to-play.shared-instance": {
"message": "Sdílená instance"
@@ -288,22 +309,22 @@
"message": "Zobrazit obsah"
},
"app.modal.update-to-play.header": {
"message": "Updatuj ke hraní"
"message": "Aktualizuj pro hraní"
},
"app.modal.update-to-play.update-required": {
"message": "Je vyžadována aktualizace"
},
"app.modal.update-to-play.update-required-description": {
"message": "Je nutná aktualizace ke hraní {name}. Prosím updatuj na nejnovější verzi k spuštění hry."
"message": "Pro hraní {name} je vyžadována aktualizace. Prosím aktualizuj na nejnovější verzi, abys mohl hru spustit."
},
"app.project.install-button.already-installed": {
"message": "Tento projekt je již nainstalován"
},
"app.project.install-context.back-to-browse": {
"message": "Zpět na Prozkoumat"
"message": "Zpět k Objevování"
},
"app.project.install-context.install-content-to-instance": {
"message": "Nainstalovat obsah do instnce"
"message": "Nainstalovat obsah do instance"
},
"app.settings.developer-mode-enabled": {
"message": "Vývojářský režim povolen."
@@ -332,21 +353,105 @@
"app.skins.add-button": {
"message": "Přidat skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Přetáhni a pusť"
},
"app.skins.apply-button": {
"message": "Použít"
},
"app.skins.delete-button": {
"message": "Smazat skin"
},
"app.skins.delete-modal.description": {
"message": "Tahle akce navždy smaže vybraný skin a nejde vrátit zpět."
},
"app.skins.delete-modal.title": {
"message": "Opravdu chceš smazat tenhle skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Nepodařilo se načíst přetažený soubor."
},
"app.skins.dropped-file-error.title": {
"message": "Chyba při načítání souboru"
},
"app.skins.edit-button": {
"message": "Upravit skin"
},
"app.skins.modal.add-skin-button": {
"message": "Přidat skin"
},
"app.skins.modal.add-title": {
"message": "Přidání skinu"
},
"app.skins.modal.arm-style-section": {
"message": "Typ paží"
},
"app.skins.modal.arm-style-slim": {
"message": "Štíhlý"
},
"app.skins.modal.arm-style-wide": {
"message": "Široký"
},
"app.skins.modal.cape-fallback-name": {
"message": "Plášť"
},
"app.skins.modal.cape-section": {
"message": "Plášť"
},
"app.skins.modal.edit-title": {
"message": "Úprava skinu"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Nejdříve uprav skin!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Bez pláště"
},
"app.skins.modal.none-cape-option": {
"message": "Žádný"
},
"app.skins.modal.replace-texture-button": {
"message": "Nahradit texturu"
},
"app.skins.modal.save-skin-button": {
"message": "Uložit skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Ukládání..."
},
"app.skins.modal.texture-section": {
"message": "Textura"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Nejdříve nahraj skin!"
},
"app.skins.preview.edit-button": {
"message": "Upravit skin"
},
"app.skins.previewing-badge": {
"message": "Náhled"
},
"app.skins.rate-limit.text": {
"message": "Měníš svůj skin příliš často. Servery Mojangu dočasně zablokovaly další požadavky. Počkej chvíli, než to zkusíš znovu."
},
"app.skins.rate-limit.title": {
"message": "Zpomal!"
},
"app.skins.reorder-error.text": {
"message": "Tvé pořadí skinů se nepodařilo uložit."
},
"app.skins.reorder-error.title": {
"message": "Nepodařilo se změnit pořadí skinů"
},
"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": "Výchozí skiny"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
@@ -356,14 +461,47 @@
"app.skins.section.modrinth-pride": {
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Tyto skiny jsi obdržel za přispění do sbírky Modrinth Pride behěm Pride měsíce."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Uložené skiny"
},
"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": "Přihlásit se"
},
"app.skins.sign-in.description": {
"message": "Přihlas se prosím ke svému Minecraft účtu, abys mohl/a využívat funkce pro správu skinů v aplikaci Modrinth."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Nadšený Modrinth Bot"
},
"app.skins.sign-in.title": {
"message": "Přihlas se prosím"
},
"app.skins.title": {
"message": "Výběr skinu"
},
"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."
"message": "Stahování aplikace Modrinth v{version} bylo dokončeno. Aktualizujte ji znovunačtem nebo se automaticky aktualizuje po zavření."
},
"app.update-popup.body.linux": {
"message": "Aplikace Modrinth v{version} je k dispozici. Aktualizujte pomocí svého správce balíčků, abyste získali nejnovější funkce a opravy!"
"message": "Aplikace Modrinth v{version} je k dispozici. Aktualizujte ji pomocí svého správce balíčků, abyste získali nejnovější funkce a opravy!"
},
"app.update-popup.body.metered": {
"message": "Aplikace Modrinth v{version} je nyní k dispozici! Protože jste v měřené síti, nebyla stažena automaticky."
@@ -378,10 +516,10 @@
"message": "Stahování bylo dokončeno"
},
"app.update-popup.reload": {
"message": "Načíst znovu"
"message": "Znovu načíst pro aktualizaci"
},
"app.update-popup.title": {
"message": "Je k dispozici aktualizace"
"message": "Aktualizace k dispozici"
},
"app.update.complete-toast.text": {
"message": "Kliknutím sem zobrazíte seznam změn."
@@ -393,13 +531,13 @@
"message": "priklad.modrinth.gg"
},
"app.world.server-modal.select-an-option": {
"message": "Vyberte možnost"
"message": "Vybrat možnost"
},
"app.world.world-item.incompatible-version": {
"message": "Nekompatibilní verze {version}"
},
"app.world.world-item.not-played-yet": {
"message": "Ještě nehráno"
"message": "Zatím nehráno"
},
"app.world.world-item.offline": {
"message": "Offline"
@@ -411,25 +549,25 @@
"message": "Přidat přítele"
},
"friends.action.view-friend-requests": {
"message": "{count} přátelé {count, plural, one {request} other {requests}}"
"message": "{count} {count, plural, one {žádost} few {žádosti} other {žádostí}} o přátelství"
},
"friends.add-friend.submit": {
"message": "Poslat žádost o přátelství"
},
"friends.add-friend.title": {
"message": "Přidákamaráda"
"message": "Přidání přítele"
},
"friends.add-friend.username.description": {
"message": "Může být jiné než jejich Minecraft jméno!"
"message": "Může se lišit od jejich Minecraft přezdívky!"
},
"friends.add-friend.username.placeholder": {
"message": "Zadejte uživatelské jméno Modrinth..."
"message": "Zadej přezdívku na Modrinthu..."
},
"friends.add-friend.username.title": {
"message": "Jak se váš kamarád jmenuje na Modrinthu?"
"message": "Jaká je přezdívka tvého přítele na Modrinthu?"
},
"friends.add-friends-to-share": {
"message": "<link>Přidejte si přátelé</link> a podívejte se, co hrají!"
"message": "<link>Přidej si přátele</link>, abys viděl, co hrají!"
},
"friends.friend.cancel-request": {
"message": "Zrušit žádost"
@@ -444,7 +582,7 @@
"message": "Zobrazit profil"
},
"friends.heading": {
"message": "Kamarádi"
"message": "Přátelé"
},
"friends.heading.active": {
"message": "Aktivní"
@@ -458,6 +596,9 @@
"friends.heading.pending": {
"message": "Čeká na vyřízení"
},
"friends.no-friends-match": {
"message": "Žádní přátelé neodpovídají ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "Hledat přátele..."
},
@@ -465,7 +606,7 @@
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Přihlašte se ke svému Modrinth účtu</link>, abyste jsi přidali přátele a uviděli co hrají!"
"message": "<link>Přihlas se k Modrinth účtu</link>, abys mohl přidávat přátele a vidět, co hrají!"
},
"instance.add-server.add-and-play": {
"message": "Přidat a hrát"
@@ -477,10 +618,10 @@
"message": "Vypnuto"
},
"instance.add-server.resource-pack.enabled": {
"message": "Zapnuto"
"message": "Povolený"
},
"instance.add-server.resource-pack.prompt": {
"message": "Ptát se"
"message": "Zeptat se"
},
"instance.add-server.title": {
"message": "Přidat server"
@@ -489,7 +630,7 @@
"message": "Upravit server"
},
"instance.edit-world.hide-from-home": {
"message": "Skrýt z domovské obrazovky"
"message": "Skrýt z domovské stránky"
},
"instance.edit-world.name": {
"message": "Název"
@@ -498,7 +639,7 @@
"message": "Minecraft svět"
},
"instance.edit-world.reset-icon": {
"message": "Resetovat ikonu"
"message": "Obnovit ikonu"
},
"instance.edit-world.title": {
"message": "Upravit svět"
@@ -516,22 +657,22 @@
"message": "Název"
},
"instance.server-modal.placeholder-name": {
"message": "Minecraft Server"
"message": "Minecraft server"
},
"instance.server-modal.resource-pack": {
"message": "Balíčky textur"
"message": "Balíček textur"
},
"instance.settings.tabs.general": {
"message": "Obecné"
},
"instance.settings.tabs.general.delete": {
"message": "Odstranit instalaci"
"message": "Odstranění instance"
},
"instance.settings.tabs.general.delete.button": {
"message": "Odstranit instalaci"
"message": "Odstranit instanci"
},
"instance.settings.tabs.general.delete.description": {
"message": "Trvale smaže instalaci z vašeho zařízení, včetně vašich světů, konfigurací a veškerého nainstalovaného obsahu. Buďte opatrní, protože jakmile instalaci smažete, nebude možné ji obnovit."
"message": "Trvale smaže instanci z tvého zařízení, včetně světů, konfigurací a veškerého nainstalovaného obsahu. Buď opatrný, jakmile instanci smažeš, nepůjde ji obnovit."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Mazání..."
@@ -540,40 +681,64 @@
"message": "Duplikovat"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Nelze duplikovat při instalaci."
"message": "Nelze duplikovat během instalace."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "Duplikovat instalaci"
"message": "Duplikace instance"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "Vytvoří kopii této instalace, včetně světů, konfigurací, modů atd."
"message": "Vytvoří kopii této instance, včetně světů, konfigurací, modů, atd."
},
"instance.settings.tabs.general.edit-icon": {
"message": "Upravit ikonu"
},
"instance.settings.tabs.general.edit-icon.remove": {
"message": "Odstranit ikonu"
"message": "Odebrat ikonu"
},
"instance.settings.tabs.general.edit-icon.replace": {
"message": "Změnit ikonu"
"message": "Nahradit ikonu"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "Vybrat ikonu"
},
"instance.settings.tabs.general.library-groups": {
"message": "Skupina knihoven"
"message": "Skupiny knihovny"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "Vytvořit novou skupinu"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "Skupiny knihoven vám umožňují uspořádat instalace do různých sekcí."
"message": "Skupiny knihovny ti umožňují uspořádat instance do různých sekcí v knihovně."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "Zadejte název skupiny"
"message": "Zadej název skupiny"
},
"instance.settings.tabs.general.name": {
"message": "Název"
"message": "Jméno"
},
"instance.settings.tabs.general.update-channel": {
"message": "Aktualizační kanál"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Vydané, beta a alfa verze se zobrazí jako dostupné aktualizace."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Vydané a beta verze se zobrazí jako dostupné aktualizace."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Vydání"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Pouze vydané verze se zobrazí jako dostupné aktualizace."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Vyber aktualizační kanál"
},
"instance.settings.tabs.hooks": {
"message": "Spouštěcí hooky"
@@ -582,28 +747,28 @@
"message": "Vlastní spouštěcí hooky"
},
"instance.settings.tabs.hooks.description": {
"message": "Hooky umožňují pokročilým uživatelům spouštět určité systémové příkazy před a po spuštění hry."
"message": "Hooky umožňují pokročilým uživatelům spustit určité systémové příkazy před a po spuštění hry."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Po ukončení"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "Spušteno po vypnutí hry."
"message": "Provede se po ukončení hry."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Zadejte příkaz k provedení po ukončení..."
"message": "Zadej příkaz k provedení po ukončení..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Před spuštěním"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Spouštějí se před spuštěním instalace."
"message": "Provede se před spuštěním instance."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "Zadejte příkaz k provedení před spuštěním..."
"message": "Zadej příkaz k provedení před spuštěním..."
},
"instance.settings.tabs.hooks.title": {
"message": "Spouštěcí hooky hry"
"message": "Spouštěcí příkazy hry"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Wrapper"
@@ -612,7 +777,7 @@
"message": "Příkaz wrapperu pro spuštění Minecraftu."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Zadejte příkaz wrapperu..."
"message": "Zadej příkaz wrapperu..."
},
"instance.settings.tabs.installation": {
"message": "Instalace"
@@ -623,6 +788,24 @@
"instance.settings.tabs.java": {
"message": "Java a pamět"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Vlastní proměnná prostředí"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Vlastní argumenty Javy"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Vlastní instalace Javy"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Vlastní nastavení paměti"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Zadej proměnná prostředí..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Zadej argumenty Javy..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Proměnné prostředí"
},
@@ -630,13 +813,16 @@
"message": "Hooky"
},
"instance.settings.tabs.java.java-arguments": {
"message": "Java argumenty"
"message": "Argumenty Javy"
},
"instance.settings.tabs.java.java-installation": {
"message": "Instalace Javy"
},
"instance.settings.tabs.java.java-memory": {
"message": "Paměti přiřazeno"
"message": "Přiřazeno paměti"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "Okno"
@@ -645,10 +831,10 @@
"message": "Vlastní nastavení okna"
},
"instance.settings.tabs.window.fullscreen": {
"message": "Celá obrazovka"
"message": "Režim celé obrazovky"
},
"instance.settings.tabs.window.fullscreen.description": {
"message": "Spustit hru v plné obrazovce pokud spuštěno (použitím options.txt)."
"message": "Při spuštění otevřít hru v režimu celé obrazovky (použitím options.txt)."
},
"instance.settings.tabs.window.height": {
"message": "Výška"
@@ -657,7 +843,7 @@
"message": "Výška herního okna při spuštění."
},
"instance.settings.tabs.window.height.enter": {
"message": "Zadejte výšku..."
"message": "Zadej výšku..."
},
"instance.settings.tabs.window.width": {
"message": "Šířka"
@@ -666,19 +852,19 @@
"message": "Šířka herního okna při spuštění."
},
"instance.settings.tabs.window.width.enter": {
"message": "Zadejte šířku..."
"message": "Zadej šířku..."
},
"instance.worlds.a_minecraft_server": {
"message": "Minecraft Server"
"message": "Minecraft server"
},
"instance.worlds.cant_connect": {
"message": "Nelze připojit k serveru"
"message": "Nelze se připojit k serveru"
},
"instance.worlds.copy_address": {
"message": "Kopírovat adresu"
},
"instance.worlds.dont_show_on_home": {
"message": "Nezobrazuje na domovské stránce"
"message": "Nezobrazovat na domovské stránce"
},
"instance.worlds.game_already_open": {
"message": "Instance je již otevřená"
@@ -693,13 +879,13 @@
"message": "Spravováno projektem serveru"
},
"instance.worlds.no_contact": {
"message": "Server se nepodařilo spojit"
"message": "Se serverem se nepodařilo spojit"
},
"instance.worlds.no_server_quick_play": {
"message": "Můžeš rovnou skočit na server pouze v Minecraftu Alpha 1.0.5+"
"message": "Připojit se rovnou k serverům můžeš pouze s Minecraft verzí Alpha 1.0.5+"
},
"instance.worlds.no_singleplayer_quick_play": {
"message": "V Minecraftu 1.20+ se dá rovnou přeskočit pouze do singleplayerových světů"
"message": "Můžeš se rovnou připojit do světů pro jednoho hráče pouze s Minecraft verzí 1.20+"
},
"instance.worlds.play_instance": {
"message": "Hrát instanci"
@@ -708,22 +894,22 @@
"message": "Zobrazit instanci"
},
"instance.worlds.world_in_use": {
"message": "Svět je použit"
"message": "Svět je používán"
},
"minecraft-account.add-account": {
"message": "Přidat účet"
},
"minecraft-account.label": {
"message": "Účet Minecraft"
"message": "Minecraft účet"
},
"minecraft-account.not-signed-in": {
"message": "Nejste přihlášeni"
},
"minecraft-account.remove-account": {
"message": "Odstranit účet"
"message": "Odebrat účet"
},
"minecraft-account.select-account": {
"message": "Vyberte účet"
"message": "Zvolte účet"
},
"minecraft-account.sign-in": {
"message": "Přihlásit se do Minecraftu"
@@ -732,31 +918,31 @@
"message": "Poskytováno instancí"
},
"search.filter.locked.instance-game-version.title": {
"message": "Verze hry je poskytnut instanci"
"message": "Verze hry je poskytována instancí"
},
"search.filter.locked.instance-loader.title": {
"message": "Spouštěč je poskytnut instancí"
"message": "Načítací modul je poskytován instancí"
},
"search.filter.locked.instance.sync": {
"message": "Synchronizováno z instancí"
"message": "Synchronizováno s instancí"
},
"search.filter.locked.server": {
"message": "Poskytováno serverem"
},
"search.filter.locked.server-environment.title": {
"message": "Jen klientské mody mohou být přidány na server"
"message": "Pouze módy ze strany klientu mohou být přidány na server"
},
"search.filter.locked.server-game-version.title": {
"message": "Verzi hry poskytuje server"
"message": "Verze hry je poskytována serverem"
},
"search.filter.locked.server-loader.title": {
"message": "Loader zprostředkovává server"
},
"unknown-pack-warning-modal.body": {
"message": "Soubor je zkontrolován pouze v případě, že je nahrán na Modrinth, a to bez ohledu na jeho formát (včetně formátu .mrpack)."
"message": "Soubor je zkontrolován pouze v případě, že je nahrán na Modrinth, bez ohledu na jeho formát (včetně formátu .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Toto varování již nezobrazovat"
"message": "Toto varování znovu nezobrazovat"
},
"unknown-pack-warning-modal.header": {
"message": "Potvrdit instalaci"
+12 -3
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Downloader Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Downloader opdatering"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Primære instance"
},
"app.action-bar.reload-to-update": {
"message": "Genindlæs for at opdater"
},
"app.action-bar.show-more-running-instances": {
"message": "Vis flere kørende instances"
},
"app.action-bar.stop-instance": {
"message": "Stop instance"
},
"app.action-bar.update": {
"message": "Opdater"
},
"app.action-bar.view-active-downloads": {
"message": "Vis aktive downloads"
},
@@ -143,6 +152,9 @@
"app.browse.server.installing": {
"message": "Installere"
},
"app.content-install.no-compatible-versions": {
"message": "Ingen tilgængelige versioner passer til {compatibilityLabel}. Vælg en version for at installere. Afhængigheder bliver ikke installeret automatisk."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -473,9 +485,6 @@
"app.update-popup.download-complete": {
"message": "Download færdiggjort"
},
"app.update-popup.reload": {
"message": "Geninlæs"
},
"app.update-popup.title": {
"message": "Opdatering tilgængelig"
},
+40 -1
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Java {version} wird heruntergeladen"
},
"app.action-bar.downloading-update": {
"message": "Update wird heruntergeladen"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Primäre Instanz"
},
"app.action-bar.reload-to-update": {
"message": "Zum Aktualisieren neu laden"
},
"app.action-bar.show-more-running-instances": {
"message": "Weitere laufende Instanzen anzeigen"
},
"app.action-bar.stop-instance": {
"message": "Instanz stoppen"
},
"app.action-bar.update": {
"message": "Aktualisieren"
},
"app.action-bar.view-active-downloads": {
"message": "Aktive Downloads anzeigen"
},
@@ -419,6 +428,12 @@
"app.skins.rate-limit.title": {
"message": "Nicht so schnell!"
},
"app.skins.reorder-error.text": {
"message": "Deine Skin-Reihenfolge konnte nicht gespeichert werden."
},
"app.skins.reorder-error.title": {
"message": "Skins konnten nicht neu angeordnet werden"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
@@ -492,7 +507,7 @@
"message": "Download abgeschlossen"
},
"app.update-popup.reload": {
"message": "Neu Laden"
"message": "Zum Aktualisieren neu laden"
},
"app.update-popup.title": {
"message": "Aktualisierung verfügbar"
@@ -692,6 +707,30 @@
"instance.settings.tabs.general.name": {
"message": "Name"
},
"instance.settings.tabs.general.update-channel": {
"message": "Update-Kanal"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Release-, Beta- und Alpha-Versionen werden als verfügbare Updates angezeigt."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Release- und Beta-Versionen werden als verfügbare Updates angezeigt."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Release"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Nur Release-Versionen werden als verfügbare Updates angezeigt."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Update-Kanal auswählen"
},
"instance.settings.tabs.hooks": {
"message": "Start Hooks"
},
+43 -4
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Java {version} wird heruntergeladen"
},
"app.action-bar.downloading-update": {
"message": "Update wird heruntergeladen"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
@@ -12,7 +15,7 @@
"message": "Zur primären Instanz machen"
},
"app.action-bar.no-instances-running": {
"message": "Keine aktiven Instanzen"
"message": "Keine laufenden Instanzen"
},
"app.action-bar.offline": {
"message": "Offline"
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Primäre Instanz"
},
"app.action-bar.reload-to-update": {
"message": "Zum Aktualisieren neu laden"
},
"app.action-bar.show-more-running-instances": {
"message": "Weitere laufende Instanzen anzeigen"
},
"app.action-bar.stop-instance": {
"message": "Instanz stoppen"
},
"app.action-bar.update": {
"message": "Aktualisieren"
},
"app.action-bar.view-active-downloads": {
"message": "Aktive Downloads anzeigen"
},
@@ -33,7 +42,7 @@
"message": "Instanz anzeigen"
},
"app.action-bar.view-logs": {
"message": "Protokolle anzeigen"
"message": "Logs anzeigen"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Aktiviert erweiterte Rendering-Funktionen wie Unschärfe-Effekte, welche ohne Hardware-beschleunigtes Rendering zu Leistungsproblemen führen können."
@@ -414,11 +423,17 @@
"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 wechselst 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!"
},
"app.skins.reorder-error.text": {
"message": "Die Anordnung deiner Skins konnte nicht gespeichert werden."
},
"app.skins.reorder-error.title": {
"message": "Skins konnten nicht neu angeordnet werden"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
@@ -492,7 +507,7 @@
"message": "Download abgeschlossen"
},
"app.update-popup.reload": {
"message": "Neu laden"
"message": "Zum Aktualisieren neu laden"
},
"app.update-popup.title": {
"message": "Aktualisierung verfügbar"
@@ -692,6 +707,30 @@
"instance.settings.tabs.general.name": {
"message": "Name"
},
"instance.settings.tabs.general.update-channel": {
"message": "Aktualisierungskanal"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Voll, Beta- und Alpha-Versionen werden als verfügbare Aktualisierungen angezeigt."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Voll- und Beta-Versionen werden als verfügbare Aktualisierungen angezeigt."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Vollversion"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Nur Vollversionen werden als verfügbare Aktualisierungen angezeigt."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Aktualisierungskanal auswählen"
},
"instance.settings.tabs.hooks": {
"message": "Startargumente"
},
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Descargando Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Descargando actualización"
},
"app.action-bar.downloads": {
"message": "Descargas"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Instancia principal"
},
"app.action-bar.reload-to-update": {
"message": "Reinicia para actualizar"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar más instancias en ejecución"
},
"app.action-bar.stop-instance": {
"message": "Detener instancia"
},
"app.action-bar.update": {
"message": "Actualizar"
},
"app.action-bar.view-active-downloads": {
"message": "Ver descargas en curso"
},
@@ -303,7 +312,7 @@
"message": "Este proyecto ya está instalado"
},
"app.project.install-context.back-to-browse": {
"message": "Volver a descubrir contenido"
"message": "Volver al explorador"
},
"app.project.install-context.install-content-to-instance": {
"message": "Instalar contenido a la instancia"
@@ -336,7 +345,7 @@
"message": "Añadir skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Arrastrar y soltar"
"message": "Arrastra y suelta"
},
"app.skins.apply-button": {
"message": "Aplicar"
@@ -345,10 +354,10 @@
"message": "Eliminar skin"
},
"app.skins.delete-modal.description": {
"message": "Esto eliminará permanentemente el skin seleccionado. Esta acción no se puede deshacer."
"message": "Esto eliminará permanentemente la skin seleccionada. Esta acción no se puede deshacer."
},
"app.skins.delete-modal.title": {
"message": "¿Estás seguro de que quieres eliminar este skin?"
"message": "¿Estás seguro de que quieres eliminar esta skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Error al leer el archivo arrastrado."
@@ -363,7 +372,7 @@
"message": "Añadir skin"
},
"app.skins.modal.add-title": {
"message": "Añadiendo un skin"
"message": "Añadiendo una skin"
},
"app.skins.modal.arm-style-section": {
"message": "Estilo de brazo"
@@ -405,7 +414,7 @@
"message": "Textura"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "¡Primero carga un skin!"
"message": "¡Primero carga una skin!"
},
"app.skins.preview.edit-button": {
"message": "Editar skin"
@@ -419,6 +428,12 @@
"app.skins.rate-limit.title": {
"message": "¡Más despacio!"
},
"app.skins.reorder-error.text": {
"message": "No se ha podido guardar el orden de las skins."
},
"app.skins.reorder-error.title": {
"message": "Error al reordenar las skins"
},
"app.skins.section.builders-and-biomes": {
"message": "Constructores y Biomas"
},
@@ -444,7 +459,7 @@
"message": "Monturas del Caos"
},
"app.skins.section.saved-skins": {
"message": "Skins guardados"
"message": "Skins guardadas"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
@@ -492,7 +507,7 @@
"message": "Descarga completada"
},
"app.update-popup.reload": {
"message": "Recargar"
"message": "Reinicia para actualizar"
},
"app.update-popup.title": {
"message": "Actualización disponible"
@@ -692,6 +707,30 @@
"instance.settings.tabs.general.name": {
"message": "Nombre"
},
"instance.settings.tabs.general.update-channel": {
"message": "Canal de actualizaciones"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Las versiones estables, beta y alpha se mostrarán como actualizaciones disponibles. "
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Las versiones estables y beta serán mostradas como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Estable"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Sólo las versiones estables serán mostradas como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Selecciona el canal de actualizaciones"
},
"instance.settings.tabs.hooks": {
"message": "Hooks de inicio"
},
@@ -882,7 +921,7 @@
"message": "Proporcionado por el servidor"
},
"search.filter.locked.server-environment.title": {
"message": "Solo se puede añadir mods que sean del lado del cliente a la instancia del servidor"
"message": "Solo se pueden añadir mods que sean del lado del cliente a la instancia del servidor"
},
"search.filter.locked.server-game-version.title": {
"message": "La versión del juego es proporcionada por el servidor"
+43 -4
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Descargando Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Descargando actualización"
},
"app.action-bar.downloads": {
"message": "Descargas"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Instancia principal"
},
"app.action-bar.reload-to-update": {
"message": "Recargar para actualizar"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar más instancias en ejecución"
},
"app.action-bar.stop-instance": {
"message": "Detener instancia"
},
"app.action-bar.update": {
"message": "Actualizar"
},
"app.action-bar.view-active-downloads": {
"message": "Mostrar descargas activas"
},
@@ -48,7 +57,7 @@
"message": "Tema de color"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Cambia la página que se abre al iniciar el lanzador."
"message": "Cambia la página en la que el launcher inicia."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Inicio"
@@ -428,6 +437,12 @@
"app.skins.rate-limit.title": {
"message": "¡Cálmate!"
},
"app.skins.reorder-error.text": {
"message": "No se ha podido guardar el orden de las skins."
},
"app.skins.reorder-error.title": {
"message": "Error al reordenar las skins"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders y Biomas"
},
@@ -501,7 +516,7 @@
"message": "Descarga completada"
},
"app.update-popup.reload": {
"message": "Recargar"
"message": "Recargar para actualizar"
},
"app.update-popup.title": {
"message": "Actualización disponible"
@@ -701,6 +716,30 @@
"instance.settings.tabs.general.name": {
"message": "Nombre"
},
"instance.settings.tabs.general.update-channel": {
"message": "Canal de actualizaciones"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Se mostrarán las versiones estables, beta y alfa como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Se mostrarán las versiones estables y beta como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Estable"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Solo se mostrarán las versiones estables como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Seleccionar canal de actualizaciones"
},
"instance.settings.tabs.hooks": {
"message": "Hooks de lanzamiento"
},
@@ -882,7 +921,7 @@
"message": "Versión del juego proporciona por la instancia"
},
"search.filter.locked.instance-loader.title": {
"message": "Cargador proporcionado por la instancia"
"message": "Loader proporcionado por la instancia"
},
"search.filter.locked.instance.sync": {
"message": "Sincronizar con la instancia"
@@ -897,7 +936,7 @@
"message": "La versión del juego es proporcionada por el servidor"
},
"search.filter.locked.server-loader.title": {
"message": "El cargador lo proporciona el servidor"
"message": "Loader proporcionado por el servidor"
},
"unknown-pack-warning-modal.body": {
"message": "Un archivo solo es revisado si es subido a Modrinth, independientemente de su formato de archivo (incluyendo .mrpack)."
+28 -1
View File
@@ -143,6 +143,9 @@
"app.browse.server.installing": {
"message": "Asennetaan"
},
"app.content-install.no-compatible-versions": {
"message": "Yhtään versiota ei ole saatavilla yhteensopivuudella {compatibilityLabel}. Valitse asennettava versio siitä huolimatta. Riippuvuuksia ei asenneta automaattisesti."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -332,6 +335,21 @@
"app.settings.tabs.resource-management": {
"message": "Resurssien hallinta"
},
"app.skins.reorder-error.title": {
"message": "Skinien järjestyksen muuttaminen epäonnistui"
},
"app.skins.section.mounts-of-mayhem": {
"message": "Sekasorron ratsut"
},
"app.skins.section.tiny-takeover": {
"message": "Pienten vallankumous"
},
"app.skins.sign-in.title": {
"message": "Kirjaudu sisään"
},
"app.skins.title": {
"message": "Skinivalinta"
},
"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."
},
@@ -351,7 +369,7 @@
"message": "Lataus valmis"
},
"app.update-popup.reload": {
"message": "Lataa uudelleen"
"message": "Lataa uudelleen päivittääksesi"
},
"app.update-popup.title": {
"message": "Päivitys saatavilla"
@@ -551,6 +569,15 @@
"instance.settings.tabs.general.name": {
"message": "Nimi"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Julkaisu"
},
"instance.settings.tabs.hooks": {
"message": "Aloitustoiminnot"
},
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Dina-download ang Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Dina-download ang update"
},
"app.action-bar.downloads": {
"message": "Mga downloads"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Pangunahing instansiya"
},
"app.action-bar.reload-to-update": {
"message": "Mag-reload upang ma-update"
},
"app.action-bar.show-more-running-instances": {
"message": "Ipakita ang mas marami pang tumatakbong instansiya"
},
"app.action-bar.stop-instance": {
"message": "Itigil ang instansiya"
},
"app.action-bar.update": {
"message": "Mag-update"
},
"app.action-bar.view-active-downloads": {
"message": "Tignan ang mga active downloads"
},
@@ -86,6 +95,9 @@
"app.appearance-settings.select-option": {
"message": "Pumili ng opsiyon"
},
"app.appearance-settings.show-play-time.description": {
"message": "Magpapakita ang katagal ng iyong paglalaro sa isang instansiya."
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Pagpapagana na mata-toggle ang sidebar."
},
@@ -377,9 +389,6 @@
"app.update-popup.download-complete": {
"message": "Nakumpleto ang pagdownload"
},
"app.update-popup.reload": {
"message": "Mag-reload"
},
"app.update-popup.title": {
"message": "May bagong update"
},
@@ -578,6 +587,12 @@
"instance.settings.tabs.general.name": {
"message": "Pangalan"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.hooks": {
"message": "Mga launch hook"
},
+41 -2
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Téléchargement de Java {version} en cours"
},
"app.action-bar.downloading-update": {
"message": "Téléchargement de la mise à jour"
},
"app.action-bar.downloads": {
"message": "Téléchargements"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Instance première"
},
"app.action-bar.reload-to-update": {
"message": "Recharger pour mettre à jour"
},
"app.action-bar.show-more-running-instances": {
"message": "Montrer plus d'instances en exécution"
},
"app.action-bar.stop-instance": {
"message": "Arrêter l'instance"
},
"app.action-bar.update": {
"message": "Mettre à jour"
},
"app.action-bar.view-active-downloads": {
"message": "Voir les téléchargements actifs"
},
@@ -93,7 +102,7 @@
"message": "Afficher le temps de jeu"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Permet d'ouvrir de fermer la barre latérale."
"message": "Permet d'ouvrir ou de fermer la barre latérale."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Tiroir latéral"
@@ -419,6 +428,12 @@
"app.skins.rate-limit.title": {
"message": "Du calme !"
},
"app.skins.reorder-error.text": {
"message": "L'ordre des skins n'a pas pu être sauvegardé."
},
"app.skins.reorder-error.title": {
"message": "Impossible de réorganiser les skins"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
@@ -492,7 +507,7 @@
"message": "Téléchargement terminé"
},
"app.update-popup.reload": {
"message": "Recharger"
"message": "Recharger pour mettre à jour"
},
"app.update-popup.title": {
"message": "Mise à jour disponible"
@@ -689,6 +704,30 @@
"instance.settings.tabs.general.name": {
"message": "Nom"
},
"instance.settings.tabs.general.update-channel": {
"message": "Canal de mise à jour"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Les versions stables, bêta et alpha seront affichées comme mises à jour disponibles."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Bêta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Les versions stables et bêta seront affichées comme mises à jour disponibles."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Stable"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Seules les versions stables seront affichées comme mises à jour disponibles."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Sélectionner le canal de mise à jour"
},
"instance.settings.tabs.hooks": {
"message": "Crochets de lancement"
},
+18 -3
View File
@@ -1,7 +1,19 @@
{
"app.action-bar.downloading-java": {
"message": "מוריד את Java {version}"
},
"app.action-bar.downloading-update": {
"message": "מוריד עדכון"
},
"app.action-bar.downloads": {
"message": "הורדות"
},
"app.action-bar.hide-more-running-instances": {
"message": "החבא יותר התקנים רצים"
},
"app.action-bar.make-primary-instance": {
"message": "הפוך למקרה ראשי"
},
"app.action-bar.offline": {
"message": "לא מקוון"
},
@@ -11,6 +23,9 @@
"app.appearance-settings.advanced-rendering.description": {
"message": "מאפשר עיבוד מתקדם כגון אפקטים טשטוש העלולים לגרום לבעיות ביצועים ללא עיבוד מואץ לחומרה."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "חזרה לעולמות"
},
"app.auth-servers.unreachable.body": {
"message": "ייתכן ששרתי האימות של Minecraft מושבתים כרגע. יש לבדוק את חיבור האינטרנט שלך ולנסות שוב מאוחר יותר."
},
@@ -221,9 +236,6 @@
"app.update-popup.download-complete": {
"message": "הורדה הושלמה"
},
"app.update-popup.reload": {
"message": "רענן"
},
"app.update-popup.title": {
"message": "עדכון זמין"
},
@@ -254,6 +266,9 @@
"friends.action.add-friend": {
"message": "הוספת חבר"
},
"friends.action.view-friend-requests": {
"message": "{count, plural, one {בקשת חברות אחת} other {# בקשות חברות}}"
},
"friends.add-friend.submit": {
"message": "שליחת בקשת חברות"
},
+41 -2
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "A Java {version} letöltése"
},
"app.action-bar.downloading-update": {
"message": "Frissítés letöltése"
},
"app.action-bar.downloads": {
"message": "Letöltések"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Elsődleges példány"
},
"app.action-bar.reload-to-update": {
"message": "Töltsd újra a frissítéshez"
},
"app.action-bar.show-more-running-instances": {
"message": "További futó pédányok megjelenítése"
},
"app.action-bar.stop-instance": {
"message": "Példány leállítása"
},
"app.action-bar.update": {
"message": "Frissítés"
},
"app.action-bar.view-active-downloads": {
"message": "Aktív letöltések megtekintése"
},
@@ -69,7 +78,7 @@
"message": "A kezdőlap „Folytasd itt” részében a legújabb világok is megtalálhatók."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "„Folytasd itt” a világokat is"
"message": "Visszaugrás a világokba"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimalizáld az indítót a Minecraft indításakor."
@@ -281,6 +290,9 @@
"app.modal.install-to-play.install-button": {
"message": "Telepítés"
},
"app.modal.install-to-play.mod-count": {
"message": "{count} mod"
},
"app.modal.install-to-play.required-modpack": {
"message": "Szükséges modcsomag"
},
@@ -498,7 +510,7 @@
"message": "Sikeres letöltés"
},
"app.update-popup.reload": {
"message": "Frissítés"
"message": "Töltsd újra a frissítéshez"
},
"app.update-popup.title": {
"message": "Frissítés elérhető"
@@ -530,6 +542,9 @@
"friends.action.add-friend": {
"message": "Barát hozzáadása"
},
"friends.action.view-friend-requests": {
"message": "{count} barát {count, plural, other {kérelem}}"
},
"friends.add-friend.submit": {
"message": "Barátkérelem elküldése"
},
@@ -695,6 +710,30 @@
"instance.settings.tabs.general.name": {
"message": "Név"
},
"instance.settings.tabs.general.update-channel": {
"message": "Frissítési csatorna"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "A kiadás, béta és az alfa verziók is meg fognak jelenni az elérhető frissítések között."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Béta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "A kiadás és a béta verziók is meg fognak jelenni az elérhető frissítések között."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Kiadás"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Csak a release verziók fognak megjelenni az elérhető frissítések között."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Válaszd ki a frissítési csatornát"
},
"instance.settings.tabs.hooks": {
"message": "Indítási horgok"
},
+55 -1
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Mengunduh Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Mengunduh pembaruan"
},
"app.action-bar.downloads": {
"message": "Unduhan"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Instans utama"
},
"app.action-bar.reload-to-update": {
"message": "Muat ulang untuk memperbarui"
},
"app.action-bar.show-more-running-instances": {
"message": "Tampilkan lebih banyak instans yang sedang berjalan"
},
"app.action-bar.stop-instance": {
"message": "Hentikan instans"
},
"app.action-bar.update": {
"message": "Perbarui"
},
"app.action-bar.view-active-downloads": {
"message": "Lihat pengunduhan berlangsung"
},
@@ -86,6 +95,12 @@
"app.appearance-settings.select-option": {
"message": "Pilih opsi"
},
"app.appearance-settings.show-play-time.description": {
"message": "Menampilkan seberapa lama Anda memainkan sebuah instans."
},
"app.appearance-settings.show-play-time.title": {
"message": "Tampilkan waktu bermain"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Menghidupkan kemampuan untuk menghidupkan dan mematikan bilah sisi."
},
@@ -143,6 +158,9 @@
"app.browse.server.installing": {
"message": "Memasang"
},
"app.content-install.no-compatible-versions": {
"message": "Tidak ada versi tersedia yang cocok untuk {compatibilityLabel}. Anda tetap dapat memilih versi yang akan dipasang. Dependensi tidak akan dipasang secara otomatis."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -272,6 +290,9 @@
"app.modal.install-to-play.install-button": {
"message": "Pasang"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# mod}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Paket mod yang diperlukan"
},
@@ -416,6 +437,12 @@
"app.skins.rate-limit.title": {
"message": "Pelan-pelan!"
},
"app.skins.reorder-error.text": {
"message": "Urutan rupa Anda tidak dapat disimpan."
},
"app.skins.reorder-error.title": {
"message": "Gagal mengurut ulang rupa"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
@@ -489,7 +516,7 @@
"message": "Selesai mengunduh"
},
"app.update-popup.reload": {
"message": "Muat ulang"
"message": "Muat ulang untuk memperbarui"
},
"app.update-popup.title": {
"message": "Pembaruan tersedia"
@@ -521,6 +548,9 @@
"friends.action.add-friend": {
"message": "Tambah teman"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, other {permintaan}} teman"
},
"friends.add-friend.submit": {
"message": "Kirim permintaan teman"
},
@@ -686,6 +716,30 @@
"instance.settings.tabs.general.name": {
"message": "Nama"
},
"instance.settings.tabs.general.update-channel": {
"message": "Perbarui kanal"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Versi rilisan, beta, dan alpha akan ditampilkan sebagai pembaruan yang tersedia."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Versi beta dan alpha akan ditampilkan sebagai pembaruan yang tersedia."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Rilisan"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Hanya versi rilisan yang akan ditampilkan sebagai pembaruan yang tersedia."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Pilih kanal pembaruan"
},
"instance.settings.tabs.hooks": {
"message": "Luncurkan kait"
},
+64 -25
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Scaricando l'aggiornamento"
},
"app.action-bar.downloads": {
"message": "Download"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Primaria"
},
"app.action-bar.reload-to-update": {
"message": "Ricarica per aggiornare"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostra più istanze in esecuzione"
},
"app.action-bar.stop-instance": {
"message": "Ferma l'istanza"
},
"app.action-bar.update": {
"message": "Aggiorna"
},
"app.action-bar.view-active-downloads": {
"message": "Mostra i download attivi"
},
@@ -90,7 +99,7 @@
"message": "Mostra quanto tempo hai speso giocando a un'istanza."
},
"app.appearance-settings.show-play-time.title": {
"message": "Mostra tempo di gioco"
"message": "Mostra le ore di gioco"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Mostra o nascondi la barra laterale."
@@ -144,13 +153,13 @@
"message": "Pacchetti di mod"
},
"app.browse.server-instance-content-warning": {
"message": "Aggiungere dei contenuti potrebbe causare problemi entrando nel server. Inoltre, qualsiasi contenuto aggiunto sarà perso a ogni aggiornamento dei contenuti dell'istanza."
"message": "Aggiungere dei contenuti potrebbe causare problemi entrando nel server. Inoltre, qualsiasi contenuto aggiunto sarà perso quando aggiorni i 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."
"message": "Nessuna versione disponibile soddisfa {compatibilityLabel}. Puoi selezionare una versione da installare comunque. Le dipendenze non verranno scaricate automaticamente."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
@@ -215,9 +224,6 @@
"app.instance.mods.project-was-added": {
"message": "\"{name}\" è stato aggiunto"
},
"app.instance.mods.projects-were-added": {
"message": "{count} progetti aggiunti"
},
"app.instance.mods.share-text": {
"message": "Guarda le mod che uso nel mio pacchetto!"
},
@@ -272,6 +278,9 @@
"app.modal.install-to-play.install-button": {
"message": "Installa"
},
"app.modal.install-to-play.mod-count": {
"message": "{count} mod"
},
"app.modal.install-to-play.required-modpack": {
"message": "Pacchetto di mod richiesto"
},
@@ -333,7 +342,7 @@
"message": "Nuova skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Trascina e rilascia"
"message": "Trascina qui"
},
"app.skins.apply-button": {
"message": "Applica"
@@ -348,7 +357,7 @@
"message": "Vuoi davvero eliminare questa skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Non è stato possibile leggere il file rilasciato."
"message": "Non è stato possibile leggere il file trascinato."
},
"app.skins.dropped-file-error.title": {
"message": "Impossibile processare il file"
@@ -357,13 +366,13 @@
"message": "Modifica"
},
"app.skins.modal.add-skin-button": {
"message": "Salva"
"message": "Aggiungi"
},
"app.skins.modal.add-title": {
"message": "Aggiungi una nuova skin"
"message": "Aggiungi una skin"
},
"app.skins.modal.arm-style-section": {
"message": "Modello del giocatore"
"message": "Modello"
},
"app.skins.modal.arm-style-slim": {
"message": "Snello"
@@ -381,7 +390,7 @@
"message": "Modifica la skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Modifica la skin prima!"
"message": "Modifica prima la skin!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Nessun mantello"
@@ -390,19 +399,19 @@
"message": "Nessuno"
},
"app.skins.modal.replace-texture-button": {
"message": "Sfoglia"
"message": "Cambia"
},
"app.skins.modal.save-skin-button": {
"message": "Salva"
},
"app.skins.modal.saving-tooltip": {
"message": "Salvataggio..."
"message": "Salvando..."
},
"app.skins.modal.texture-section": {
"message": "File della skin"
"message": "Aspetto"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Carica una skin prima!"
"message": "Carica prima una skin!"
},
"app.skins.preview.edit-button": {
"message": "Modifica"
@@ -411,11 +420,17 @@
"message": "Anteprima"
},
"app.skins.rate-limit.text": {
"message": "Stai modificando la skin troppo spesso. I server di Mojang hanno limitato le nostre richieste. Aspetta un momento prima di riprovare."
"message": "Stai cambiando skin troppo spesso. I server di Mojang hanno limitato le nostre richieste. Aspetta un momento prima di riprovare."
},
"app.skins.rate-limit.title": {
"message": "Rallenta!"
},
"app.skins.reorder-error.text": {
"message": "L'ordine delle skin non è stato salvato."
},
"app.skins.reorder-error.title": {
"message": "Errore nel riordine delle skin"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
@@ -441,7 +456,7 @@
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Raccolta"
"message": "Skin salvate"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
@@ -459,13 +474,13 @@
"message": "Accedi"
},
"app.skins.sign-in.description": {
"message": "Accedi col tuo account Minecraft per usare la gestione delle skin."
"message": "Accedi col tuo account Minecraft per poter gestire le skin qui."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Modrinth Bot emozionato"
},
"app.skins.sign-in.title": {
"message": "Accedi"
"message": "Effettua l'accesso"
},
"app.skins.title": {
"message": "Seleziona una skin"
@@ -522,7 +537,7 @@
"message": "Stringi un'amicizia"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {richiesta} other {richieste}} d''amicizia"
"message": "{count, plural, one {# richiesta} other {# richieste}} d''amicizia"
},
"friends.add-friend.submit": {
"message": "Invia richiesta d'amicizia"
@@ -689,6 +704,30 @@
"instance.settings.tabs.general.name": {
"message": "Nome"
},
"instance.settings.tabs.general.update-channel": {
"message": "Canale di aggiornamento"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Le versioni stabili, beta e alpha saranno mostrate come aggiornamenti disponibili."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Le versioni stabili e beta saranno mostrate come aggiornamenti disponibili."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Stabile"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Solo le versioni stabili saranno mostrate come aggiornamenti disponibili."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Seleziona il canale di aggiornamento"
},
"instance.settings.tabs.hooks": {
"message": "Hook di avvio"
},
@@ -738,16 +777,16 @@
"message": "Java e memoria"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variabili d'ambiente"
"message": "Personalizzate"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Argomenti JVM"
"message": "Personalizzati"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Eseguibile di Java"
"message": "Personalizzata"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Memoria allocata"
"message": "Personalizzata"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Inserisci le variabili d'ambiente..."
+75 -12
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Java {version} をダウンロード中"
},
"app.action-bar.downloading-update": {
"message": "アップデートをダウンロード中"
},
"app.action-bar.downloads": {
"message": "ダウンロード"
},
@@ -42,7 +45,7 @@
"message": "高度なレンダリング"
},
"app.appearance-settings.color-theme.description": {
"message": "Modrinth Appでのお好みのテーマを選択してください"
"message": "Modrinth Appでのお好みのテーマを選択してください"
},
"app.appearance-settings.color-theme.title": {
"message": "テーマ"
@@ -60,25 +63,25 @@
"message": "デフォルトの起動ページ"
},
"app.appearance-settings.hide-nametag.description": {
"message": "スキンページでプレイヤー名の上にある名前タグを無効にします。"
"message": "スキンページでプレイヤー上部のネームタグを非表示にします。"
},
"app.appearance-settings.hide-nametag.title": {
"message": "名札を非表示にする"
"message": "ネームタグを非表示にする"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "ホームページの「ジャンプバック」セクションには、最近追加されたワールドが含まれています。"
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "再び世界へ飛び込もう"
"message": "最近のワールドを表示"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minecraftが起動した時、ランチャーを最小化します"
"message": "Minecraftが起動した時、ランチャーを最小化します"
},
"app.appearance-settings.minimize-launcher.title": {
"message": "ランチャーを最小化"
},
"app.appearance-settings.native-decorations.description": {
"message": "OSデフォルトのウィンドウUIを使用します(アプリの再起動が必要です)"
"message": "OSデフォルトのウィンドウUIを使用します。(アプリの再起動が必要です"
},
"app.appearance-settings.native-decorations.title": {
"message": "OSのウィンドウUIを使用"
@@ -86,17 +89,23 @@
"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": "サイドバーの表示/非表示を切り替える機能を有効にします。"
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "サイドバーの表示/非表示を切り替える"
"message": "サイドバーの表示切替を有効化"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Modrinth上にホストされていないModrinth Packファイル.mrpack)をインストールしようとした場合、インストール前にそのリスクを理解していただけるよう確認します。"
"message": "Modrinthで配布されていないModrinth Pack.mrpack)をインストールする場合、事前にリスクについて警告します。"
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "未知のMODパックインストールする前に警告してください"
"message": "不明なModパックインストール前に警告する"
},
"app.auth-servers.unreachable.body": {
"message": "Minecraftの認証サーバーは現在停止している可能性があります。インターネット接続を確認し、しばらくしてからもう一度お試しください。"
@@ -254,6 +263,9 @@
"app.modal.install-to-play.install-button": {
"message": "インストール"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {#個のMod}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "必須のModパック"
},
@@ -302,6 +314,21 @@
"app.settings.tabs.resource-management": {
"message": "リソース管理"
},
"app.skins.modal.arm-style-slim": {
"message": "スリム"
},
"app.skins.modal.arm-style-wide": {
"message": "ワイド"
},
"app.skins.modal.cape-fallback-name": {
"message": "マント"
},
"app.skins.modal.cape-section": {
"message": "マント"
},
"app.skins.modal.no-cape-tooltip": {
"message": "マントなし"
},
"app.skins.modal.replace-texture-button": {
"message": "置きかえる"
},
@@ -311,6 +338,42 @@
"app.skins.preview.edit-button": {
"message": "編集"
},
"app.skins.section.builders-and-biomes": {
"message": "ビルダーズ&バイオーム"
},
"app.skins.section.chase-the-skies": {
"message": "チェイス・ザ・スカイ"
},
"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.mounts-of-mayhem": {
"message": "マウント・オブ・メイヘム"
},
"app.skins.section.saved-skins": {
"message": "保存済みのスキン"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "銅の時代"
},
"app.skins.section.the-garden-awakens": {
"message": "ガーデン・アウェイケンズ"
},
"app.skins.section.tiny-takeover": {
"message": "タイニー・テイクオーバー"
},
"app.skins.sign-in.rinthbot-alt": {
"message": "ワクワクしている Modrinth ボット"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} のダウンロードが完了しました。今すぐ更新するには再読み込みするか、Modrinth Appを閉じる際に自動更新されます。"
},
@@ -329,9 +392,6 @@
"app.update-popup.download-complete": {
"message": "ダウンロード完了"
},
"app.update-popup.reload": {
"message": "再読み込み"
},
"app.update-popup.title": {
"message": "アップデートが利用可能です"
},
@@ -362,6 +422,9 @@
"friends.action.add-friend": {
"message": "フレンドを追加"
},
"friends.action.view-friend-requests": {
"message": "{count}件の友達リクエスト"
},
"friends.add-friend.submit": {
"message": "フレンド申請を送信"
},
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Java {version} 다운로드 중"
},
"app.action-bar.downloading-update": {
"message": "업데이트 다운로드중"
},
"app.action-bar.downloads": {
"message": "다운로드"
},
@@ -500,9 +503,6 @@
"app.update-popup.download-complete": {
"message": "다운로드 완료"
},
"app.update-popup.reload": {
"message": "새로고침"
},
"app.update-popup.title": {
"message": "업데이트 가능"
},
@@ -533,6 +533,9 @@
"friends.action.add-friend": {
"message": "친구 추가"
},
"friends.action.view-friend-requests": {
"message": "{count}건의 친구 요청"
},
"friends.add-friend.submit": {
"message": "친구 요청 보내기"
},
@@ -275,6 +275,9 @@
"app.modal.install-to-play.install-button": {
"message": "Pasang"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# mod}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Pek mod yang diperlukan"
},
@@ -455,9 +458,6 @@
"app.update-popup.download-complete": {
"message": "Muat turun selesai"
},
"app.update-popup.reload": {
"message": "Muat semula"
},
"app.update-popup.title": {
"message": "Kemas kini tersedia"
},
@@ -488,6 +488,9 @@
"friends.action.add-friend": {
"message": "Tambah rakan"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, other {permintaan}} rakan"
},
"friends.add-friend.submit": {
"message": "Hantar permintaan rakan"
},
+217 -1
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Java {version} aan het downloaden"
},
"app.action-bar.downloading-update": {
"message": "Update aan het downloaden"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Primaire instantie"
},
"app.action-bar.reload-to-update": {
"message": "Herlaadt om te updaten"
},
"app.action-bar.show-more-running-instances": {
"message": "Toon meer actieve instanties"
},
"app.action-bar.stop-instance": {
"message": "Stop instantie"
},
"app.action-bar.update": {
"message": "Updaten"
},
"app.action-bar.view-active-downloads": {
"message": "Bekijk actieve downloads"
},
@@ -86,6 +95,12 @@
"app.appearance-settings.select-option": {
"message": "Selecteer een optie"
},
"app.appearance-settings.show-play-time.description": {
"message": "Toont hoeveel tijd je aan een instantie hebt besteed."
},
"app.appearance-settings.show-play-time.title": {
"message": "Speeltijd laten zien"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Schakelt de mogelijkheid om de zijbalk in- of uit te schakelen in."
},
@@ -143,6 +158,9 @@
"app.browse.server.installing": {
"message": "Installeren"
},
"app.content-install.no-compatible-versions": {
"message": "Er zijn geen beschikbare versies die overeenkomen met {compatibilityLabel}. Selecteer toch een versie om te installeren. Afhankelijkheden worden niet automatisch geïnstalleerd."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -254,6 +272,9 @@
"app.instance.worlds.remove-server-title": {
"message": "Ben je zeker dat je {name} wil verwijderen?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Zoek werelden"
},
"app.instance.worlds.this-server": {
"message": "deze server"
},
@@ -296,6 +317,9 @@
"app.project.install-button.already-installed": {
"message": "Dit project is al geïnstalleerd"
},
"app.project.install-context.back-to-browse": {
"message": "Terug naar Ontdekken"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installeer content naar instantie"
},
@@ -323,6 +347,153 @@
"app.settings.tabs.resource-management": {
"message": "Bronnenbeheer"
},
"app.skins.add-button": {
"message": "Skin toevoegen"
},
"app.skins.add-button.drag-and-drop": {
"message": "Slepen en neerzetten"
},
"app.skins.apply-button": {
"message": "Toepassen"
},
"app.skins.delete-button": {
"message": "Skin verwijderen"
},
"app.skins.delete-modal.description": {
"message": "De geselecteerde skin wordt permanent verwijderd. Deze actie kan niet ongedaan worden gemaakt."
},
"app.skins.delete-modal.title": {
"message": "Weet u zeker dat u deze skin wilt verwijderen?"
},
"app.skins.dropped-file-error.text": {
"message": "Kan het neergezette bestand niet lezen."
},
"app.skins.dropped-file-error.title": {
"message": "Fout bij het verwerken van het bestand"
},
"app.skins.edit-button": {
"message": "Skin aanpassen"
},
"app.skins.modal.add-skin-button": {
"message": "Skin toevoegen"
},
"app.skins.modal.add-title": {
"message": "Voeg een skin toe"
},
"app.skins.modal.arm-style-section": {
"message": "Armstijl"
},
"app.skins.modal.arm-style-slim": {
"message": "Slank"
},
"app.skins.modal.arm-style-wide": {
"message": "Breed"
},
"app.skins.modal.cape-fallback-name": {
"message": "Cape"
},
"app.skins.modal.cape-section": {
"message": "Cape"
},
"app.skins.modal.edit-title": {
"message": "Skin aan het aanpassen"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Verander eerst iets aan de skin!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Geen cape"
},
"app.skins.modal.none-cape-option": {
"message": "Geen"
},
"app.skins.modal.replace-texture-button": {
"message": "Texture vervangen"
},
"app.skins.modal.save-skin-button": {
"message": "Skin opslaan"
},
"app.skins.modal.saving-tooltip": {
"message": "Opslaan..."
},
"app.skins.modal.texture-section": {
"message": "Textuur"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Upload eerst een skin!"
},
"app.skins.preview.edit-button": {
"message": "Skin aanpassen"
},
"app.skins.previewing-badge": {
"message": "Voorvertonen"
},
"app.skins.rate-limit.text": {
"message": "U verandert te snel van skin. Mojang's servers hebben nieuwe aanvragen tijdelijk geblokkeerd. Wacht een moment voordat u het opnieuw probeert."
},
"app.skins.rate-limit.title": {
"message": "Rustig aan!"
},
"app.skins.reorder-error.text": {
"message": "Je skinvolgorde kon niet worden opgeslagen."
},
"app.skins.reorder-error.title": {
"message": "Het herschikken van skins is mislukt."
},
"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": "Standaardskins"
},
"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": "U heeft deze skins ontvangen omdat u tijdens Pride Month heeft gedoneerd aan een Modrinth Pride-inzamelingsactie."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Opgeslagen 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": "Inloggen"
},
"app.skins.sign-in.description": {
"message": "Log in op je Minecraft-account om de skinbeheerfuncties van de Modrinth-app te gebruiken."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Excited Modrinth Bot"
},
"app.skins.sign-in.title": {
"message": "Log in"
},
"app.skins.title": {
"message": "Skinkiezer"
},
"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."
},
@@ -342,7 +513,7 @@
"message": "Downloaden voltooid"
},
"app.update-popup.reload": {
"message": "Herlaad"
"message": "Herladen om te updaten"
},
"app.update-popup.title": {
"message": "Update beschikbaar"
@@ -542,6 +713,30 @@
"instance.settings.tabs.general.name": {
"message": "Naam"
},
"instance.settings.tabs.general.update-channel": {
"message": "Updatekanaal"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Release-, beta- en alfaversies worden als beschikbare updates weergegeven."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Release- en beta-versies worden als beschikbare updates weergegeven.\n"
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Release"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Alleen releaseversies worden als beschikbare updates weergegeven."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Selecteer updatekanaal"
},
"instance.settings.tabs.hooks": {
"message": "Opstart haakjes"
},
@@ -590,6 +785,24 @@
"instance.settings.tabs.java": {
"message": "Java en geheugen"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Aangepaste omgevingsvariabelen"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Aangepaste Java-argumenten"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Aangepaste Java-installatie"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Aangepaste geheugentoewijzing"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Voer omgevingsvariabelen in…"
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Voer Java-argumenten in…"
},
"instance.settings.tabs.java.environment-variables": {
"message": "Environment variabelen"
},
@@ -605,6 +818,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Voorgeschreven geheugen"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "//pad/naar/java"
},
"instance.settings.tabs.window": {
"message": "Venster"
},
+47 -2
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Pobieranie Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Pobieranie aktualizacji"
},
"app.action-bar.downloads": {
"message": "Pobrania"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Główna instancja"
},
"app.action-bar.reload-to-update": {
"message": "Załaduj ponownie, by zaktualizować"
},
"app.action-bar.show-more-running-instances": {
"message": "Pokaż więcej włączonych instancji"
},
"app.action-bar.stop-instance": {
"message": "Zatrzymaj instancję"
},
"app.action-bar.update": {
"message": "Zaktualizuj"
},
"app.action-bar.view-active-downloads": {
"message": "Pokaż aktualnie pobierane"
},
@@ -270,7 +279,7 @@
"message": "ten serwer"
},
"app.modal.install-to-play.content-required": {
"message": "Wymagane treści"
"message": "Wymagana zawartość"
},
"app.modal.install-to-play.header": {
"message": "Zainstaluj, aby grać"
@@ -278,6 +287,9 @@
"app.modal.install-to-play.install-button": {
"message": "Zainstaluj"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} few {# mody} other {# modów}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Wymagana paczka modów"
},
@@ -422,6 +434,12 @@
"app.skins.rate-limit.title": {
"message": "Zwolnij!"
},
"app.skins.reorder-error.text": {
"message": "Nie udało się zapisać kolejności skórek."
},
"app.skins.reorder-error.title": {
"message": "Nie udało się zmienić kolejności skórek"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
@@ -495,7 +513,7 @@
"message": "Pobieranie ukończone"
},
"app.update-popup.reload": {
"message": "Załaduj ponownie"
"message": "Załaduj ponownie, by zaktualizować"
},
"app.update-popup.title": {
"message": "Dostępna aktualizacja"
@@ -527,6 +545,9 @@
"friends.action.add-friend": {
"message": "Dodaj znajomego"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {zaproszenie} few {zaproszenia} other {zaproszeń}} do znajomych"
},
"friends.add-friend.submit": {
"message": "Wyślij zaproszenie"
},
@@ -692,6 +713,30 @@
"instance.settings.tabs.general.name": {
"message": "Nazwa"
},
"instance.settings.tabs.general.update-channel": {
"message": "Kanał aktualizacji"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Wersje stabilne, beta i alfa będą pokazane jako dostępne aktualizacje."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Tylko wersje stabilne i wersje beta będą pokazane jako dostępne aktualizacje."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Stabilny"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Tylko wersje stabilne będą pokazane jako dostępne aktualizacje."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Wybierz kanał aktualizacji"
},
"instance.settings.tabs.hooks": {
"message": "Haczyki startowe"
},
+77 -35
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Baixando Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Baixando atualização"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Instância principal"
},
"app.action-bar.reload-to-update": {
"message": "Recarregar para atualizar"
},
"app.action-bar.show-more-running-instances": {
"message": "Exibir mais instâncias em execução"
},
"app.action-bar.stop-instance": {
"message": "Parar instância"
},
"app.action-bar.update": {
"message": "Atualizar"
},
"app.action-bar.view-active-downloads": {
"message": "Ver downloads ativos"
},
@@ -42,7 +51,7 @@
"message": "Renderização avançada"
},
"app.appearance-settings.color-theme.description": {
"message": "Selecione seu tema de cores preferido para o Modrinth App."
"message": "Escolha seu tema de cores preferido para o Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema de cores"
@@ -99,7 +108,7 @@
"message": "Alternar barra lateral"
},
"app.appearance-settings.unknown-pack-warning.description": {
"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."
"message": "Se você tentar instalar um arquivo Modrinth Pack (.mrpack) não hospedado no Modrinth, garantiremos que você entenda os riscos antes de instalar."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Avise-me antes de instalar pacotes de mods desconhecidos"
@@ -281,6 +290,9 @@
"app.modal.install-to-play.install-button": {
"message": "Instalar"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, =0 {Nenhum mod} one {# mod} other {# mods}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Pacote de mods necessário"
},
@@ -425,6 +437,12 @@
"app.skins.rate-limit.title": {
"message": "Mais lento!"
},
"app.skins.reorder-error.text": {
"message": "Não foi possível salvar a ordem de skins."
},
"app.skins.reorder-error.title": {
"message": "Falhou ao reordenar skins"
},
"app.skins.section.builders-and-biomes": {
"message": "Construtores & Biomas"
},
@@ -456,7 +474,7 @@
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "A Era do Cobre"
"message": "The Copper Age"
},
"app.skins.section.the-garden-awakens": {
"message": "The Garden Awakens"
@@ -468,7 +486,7 @@
"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."
"message": "Inicie sessão na sua conta do Minecraft para usar os recursos de gerenciamento de skins no Modrinth App."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Modrinth Bot animado"
@@ -480,10 +498,10 @@
"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."
"message": "O Modrinth App v{version} foi instalado. Recarregue para atualizar agora ou ela será feita automaticamente ao fechar o Modrinth App."
},
"app.update-popup.body.linux": {
"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!"
"message": "O Modrinth App v{version} está disponível. Use o gerenciador de pacotes para atualizar para obter novos recursos e correções!"
},
"app.update-popup.body.metered": {
"message": "O Modrinth App v{version} já está disponível! Já que sua rede é limitada, não a instalamos automaticamente."
@@ -498,7 +516,7 @@
"message": "Instalação concluída"
},
"app.update-popup.reload": {
"message": "Recarregar"
"message": "Recarregar para atualizar"
},
"app.update-popup.title": {
"message": "Atualização disponível"
@@ -552,13 +570,13 @@
"message": "<link>Adicione amigos</link> para ver o que eles estão jogando!"
},
"friends.friend.cancel-request": {
"message": "Cancelar solicitação"
"message": "Cancelar pedido"
},
"friends.friend.remove-friend": {
"message": "Remover amigo"
},
"friends.friend.request-sent": {
"message": "Solicitação de amizade enviada"
"message": "Pedido de amizade enviado"
},
"friends.friend.view-profile": {
"message": "Ver perfil"
@@ -612,7 +630,7 @@
"message": "Editar servidor"
},
"instance.edit-world.hide-from-home": {
"message": "Esconder da página inicial"
"message": "Ocultar da página inicial"
},
"instance.edit-world.name": {
"message": "Nome"
@@ -654,7 +672,7 @@
"message": "Excluir instância"
},
"instance.settings.tabs.general.delete.description": {
"message": "Exclui permanentemente uma instância do seu dispositivo, incluindo seus mundos, configurações e todo o conteúdo instalado. Tome cuidado, pois, uma vez excluída, a instância não pode ser recuperada."
"message": "Exclui permanentemente uma instância do seu dispositivo, incluindo seus mundos, configs e todo o conteúdo instalado. Tome cuidado, assim que excluído, a instância não pode ser recuperada."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Excluindo..."
@@ -669,7 +687,7 @@
"message": "Duplicar instância"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "Cria uma cópia desta instância, incluindo mundos, configurações, mods, etc."
"message": "Cria uma cópia desta instância, incluindo mundos, configs, mods, etc."
},
"instance.settings.tabs.general.edit-icon": {
"message": "Editar ícone"
@@ -687,7 +705,7 @@
"message": "Grupos"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "Criar novo grupo"
"message": "Criar grupo novo"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "Os grupos da biblioteca permitem que você organize suas instâncias em diferentes seções."
@@ -698,6 +716,30 @@
"instance.settings.tabs.general.name": {
"message": "Nome"
},
"instance.settings.tabs.general.update-channel": {
"message": "Canal de atualização"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "As versões release, beta e alpha aparecerão como atualizações disponíveis."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "As versões release e beta aparecerão como atualizações disponíveis."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Release"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Apenas versões release aparecerão como atualizações disponíveis."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Selecionar canal de atualização"
},
"instance.settings.tabs.hooks": {
"message": "Ações de inicialização"
},
@@ -705,7 +747,7 @@
"message": "Ações de inicialização personalizadas"
},
"instance.settings.tabs.hooks.description": {
"message": "Essas ações permitem que usuários mais experientes executem comandos do sistema antes e depois de inicializar o jogo."
"message": "As ações permitem usuários experientes que executem certos comandos do sistema antes ou após iniciar o jogo."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Ao sair"
@@ -714,16 +756,16 @@
"message": "Executado após o jogo fechar."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Insira o comando a ser executado após o jogo fechar..."
"message": "Insira o comando ao sair..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Pré-inicialização"
"message": "Ao iniciar"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Executado antes que a instância seja inicializada."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "Insira o comando a ser executado antes da inicialização..."
"message": "Insira o comando ao iniciar..."
},
"instance.settings.tabs.hooks.title": {
"message": "Ações de inicialização do jogo"
@@ -735,13 +777,13 @@
"message": "Comando auxiliar para iniciar o Minecraft."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Insira um comando..."
"message": "Insira um comando auxiliar..."
},
"instance.settings.tabs.installation": {
"message": "Instalação"
},
"instance.settings.tabs.installation.loader-version": {
"message": "Versão do {loader}"
"message": "Versão {loader}"
},
"instance.settings.tabs.java": {
"message": "Java e memória"
@@ -786,13 +828,13 @@
"message": "Janela"
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "Configurações de janela personalizada"
"message": "Opções de janela personalizada"
},
"instance.settings.tabs.window.fullscreen": {
"message": "Tela cheia"
},
"instance.settings.tabs.window.fullscreen.description": {
"message": "Faz com que o jogo inicie em tela cheia (usando options.txt)."
"message": "Faz o jogo iniciar em tela cheia (usando options.txt)."
},
"instance.settings.tabs.window.height": {
"message": "Altura"
@@ -822,7 +864,7 @@
"message": "Copiar endereço"
},
"instance.worlds.dont_show_on_home": {
"message": "Não mostrar no início"
"message": "Não exibir na página inicial"
},
"instance.worlds.game_already_open": {
"message": "A instância já está aberta"
@@ -834,19 +876,19 @@
"message": "Servidor incompatível"
},
"instance.worlds.linked_server": {
"message": "Gerenciado pelo projeto de servidor"
"message": "Gerenciado pelo projeto do servidor"
},
"instance.worlds.no_contact": {
"message": "Não foi possível conectar-se ao servidor"
},
"instance.worlds.no_server_quick_play": {
"message": "Você só pode entrar diretamente em servidores a partir do Minecraft 1.0.5"
"message": "Só é possível entrar diretamente em servidores a partir do Minecraft Alpha 1.0.5"
},
"instance.worlds.no_singleplayer_quick_play": {
"message": "Você só pode entrar diretamente em mundos de um jogador a partir do Minecraft 1.20+"
"message": "Só é possível entrar em mundos um jogador diretamente a partir do Minecraft 1.20"
},
"instance.worlds.play_instance": {
"message": "Jogar na instância"
"message": "Iniciar instância"
},
"instance.worlds.view_instance": {
"message": "Ver instância"
@@ -858,7 +900,7 @@
"message": "Adicionar conta"
},
"minecraft-account.label": {
"message": "Conta do minecraft"
"message": "Conta de Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Não conectado"
@@ -870,7 +912,7 @@
"message": "Selecionar conta"
},
"minecraft-account.sign-in": {
"message": "Faça login no Minecraft"
"message": "Inicie sessão no Minecraft"
},
"search.filter.locked.instance": {
"message": "Fornecido pela instância"
@@ -879,7 +921,7 @@
"message": "A versão do jogo é fornecida pela instância"
},
"search.filter.locked.instance-loader.title": {
"message": "O carregador é fornecido pela instância"
"message": "O loader é fornecido pela instância"
},
"search.filter.locked.instance.sync": {
"message": "Sincronizar com a instância"
@@ -888,19 +930,19 @@
"message": "Fornecido pelo servidor"
},
"search.filter.locked.server-environment.title": {
"message": "Apenas mods do lado do cliente podem ser adicionados à instância do servidor"
"message": "Somente mods do cliente podem ser adicionados à instância do servidor"
},
"search.filter.locked.server-game-version.title": {
"message": "A versão do jogo é fornecida pelo servidor"
},
"search.filter.locked.server-loader.title": {
"message": "O carregador é fornecido pelo servidor"
"message": "O loader é fornecido pelo servidor"
},
"unknown-pack-warning-modal.body": {
"message": "Um arquivo só é revisado se for enviado para o Modrinth, não importa o seu formato de arquivo (incluindo .mrpack)."
"message": "Um arquivo só é revisado se for enviado no Modrinth, independente do formato do arquivo (incluindo .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Não mostrar este aviso novamente"
"message": "Não exibir aviso novamente"
},
"unknown-pack-warning-modal.header": {
"message": "Confirmar instalação"
@@ -909,10 +951,10 @@
"message": "Instalar mesmo assim"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Malware é frequentemente distribuído através de arquivos de pacotes de mods compartilhados em plataformas como o Discord."
"message": "O malware é distribuído frequentemente através de arquivos de pacote de mods compartilhados em plataformas como Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Não foi possível encontrar este arquivo no Modrinth. Recomendamos fortemente que você instale apenas arquivos de fontes confiáveis."
"message": "Não encontramos este arquivo no Modrinth. Nós recomendamos fortemente instalar arquivos apenas de fontes confiáveis."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Aviso de arquivo desconhecido"
+30 -3
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "A descarregar a versão do java {version}"
},
"app.action-bar.downloading-update": {
"message": "A transferir atualização"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Instância principal"
},
"app.action-bar.reload-to-update": {
"message": "Recarregar para atualizar"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar mais instâncias a correr"
},
"app.action-bar.stop-instance": {
"message": "Para instância"
},
"app.action-bar.update": {
"message": "Atualizar"
},
"app.action-bar.view-active-downloads": {
"message": "Ver instâncias ativas"
},
@@ -35,6 +44,9 @@
"app.action-bar.view-logs": {
"message": "Ver logs"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Ativa a renderização avançada, como efeitos de desfoque, que podem causar problemas de desempenho sem o uso de renderização acelerada por hardware."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Renderização avançada"
},
@@ -56,6 +68,12 @@
"app.appearance-settings.default-landing-page.title": {
"message": "Página de carregamento principal"
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimizar launcher"
},
"app.appearance-settings.select-option": {
"message": "Seleciona uma opção"
},
"app.auth-servers.unreachable.body": {
"message": "Os servidores de autenticação do Minecraft poderão estar em baixo de momento. Verifica a tua ligação à internet e tenta novamente mais tarde."
},
@@ -80,6 +98,9 @@
"app.browse.discover-servers": {
"message": "Descobrir servidores"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.export-modal.description-placeholder": {
"message": "Insere a descrição do modpack..."
},
@@ -239,6 +260,15 @@
"app.settings.tabs.resource-management": {
"message": "Gestão de recursos"
},
"app.skins.delete-modal.title": {
"message": "Tens a certeza que queres apagar esta skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Não foi possível ler o ficheiro largado."
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"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."
},
@@ -257,9 +287,6 @@
"app.update-popup.download-complete": {
"message": "Transferência concluída"
},
"app.update-popup.reload": {
"message": "Recarregar"
},
"app.update-popup.title": {
"message": "Atualização disponível"
},
@@ -83,9 +83,6 @@
"app.update-popup.download-complete": {
"message": "Descărcare finalizată"
},
"app.update-popup.reload": {
"message": "Reîncărcați"
},
"app.update-popup.title": {
"message": "Actualizare disponibilă"
},
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Скачивание Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Скачивание"
},
"app.action-bar.downloads": {
"message": "Загрузки"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Основная сборка"
},
"app.action-bar.reload-to-update": {
"message": "Перезапустить"
},
"app.action-bar.show-more-running-instances": {
"message": "Показать другие активные сборки"
},
"app.action-bar.stop-instance": {
"message": "Остановить сборку"
},
"app.action-bar.update": {
"message": "Обновить"
},
"app.action-bar.view-active-downloads": {
"message": "Посмотреть текущие загрузки"
},
@@ -266,6 +275,9 @@
"app.modal.install-to-play.install-button": {
"message": "Установить"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# мод} few {# мода} other {# модов}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Необходимая сборка"
},
@@ -410,6 +422,12 @@
"app.skins.rate-limit.title": {
"message": "Не так быстро!"
},
"app.skins.reorder-error.text": {
"message": "Ваш порядок скинов не удалось сохранить."
},
"app.skins.reorder-error.title": {
"message": "Не удалось изменить порядок скинов"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
@@ -515,6 +533,9 @@
"friends.action.add-friend": {
"message": "Добавить в друзья"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {запрос} few {запроса} other {запросов}} дружбы"
},
"friends.add-friend.submit": {
"message": "Отправить запрос дружбы"
},
@@ -677,6 +698,30 @@
"instance.settings.tabs.general.name": {
"message": "Название"
},
"instance.settings.tabs.general.update-channel": {
"message": "Канал обновления"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Альфа"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Релизные, бета и альфа версии будут отображаться как доступные обновления."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Бета"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Релизные и бета версии будут отображаться как доступные обновления."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Релиз"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Только релизные версии будут отображаться как доступные обновления."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Выберите канал обновления"
},
"instance.settings.tabs.hooks": {
"message": "Команды запуска"
},
+190 -7
View File
@@ -2,17 +2,20 @@
"app.action-bar.downloading-java": {
"message": "Laddar ner Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Laddar ner uppdatering"
},
"app.action-bar.downloads": {
"message": "Nedladdningar"
},
"app.action-bar.hide-more-running-instances": {
"message": "Göm fler pågående instanser"
"message": "Göm fler aktiva instanser"
},
"app.action-bar.make-primary-instance": {
"message": "Gör till primärinstans"
},
"app.action-bar.no-instances-running": {
"message": "Ingen pågående instans"
"message": "Ingen aktiva instans"
},
"app.action-bar.offline": {
"message": "Offline"
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Primärinstans"
},
"app.action-bar.reload-to-update": {
"message": "Ladda om för att uppdatera"
},
"app.action-bar.show-more-running-instances": {
"message": "Visa fler pågående instanser"
"message": "Visa fler aktiva instanser"
},
"app.action-bar.stop-instance": {
"message": "Avsluta instans"
},
"app.action-bar.update": {
"message": "Uppdatera"
},
"app.action-bar.view-active-downloads": {
"message": "Visa aktiva nedladdningar"
},
@@ -65,6 +74,9 @@
"app.appearance-settings.hide-nametag.title": {
"message": "Dölj namnskylt"
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Hoppa tillbaka i världar"
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimera programstartare"
},
@@ -77,18 +89,33 @@
"app.appearance-settings.select-option": {
"message": "Välj ett alternativ"
},
"app.appearance-settings.show-play-time.description": {
"message": "Visar hur mycket tid du spelat på en instans."
},
"app.appearance-settings.show-play-time.title": {
"message": "Visa speltid"
},
"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.appearance-settings.unknown-pack-warning.description": {
"message": "Om du försöker installera en Modrinth Pack fil (.mrpack) som inte kommer från Modrinth ser vi till att du känner till riskerna innan du installerar den."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Varna mig innan jag installerar okända modpaket"
},
"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-servers-to-instance": {
"message": "Lägger till server till instans"
},
"app.browse.add-to-an-instance": {
"message": "Lägg till i en instans"
},
@@ -122,6 +149,9 @@
"app.browse.server.installing": {
"message": "Installerar"
},
"app.content-install.no-compatible-versions": {
"message": "Ingen tillgänglig version matchar {compatibilityLabel}. Välj en version att installera ändå. Beroenden kommer inte installeras automatiskt."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -146,6 +176,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Modpaketets namn"
},
"app.export-modal.select-files-label": {
"message": "Konfigurera vilka filer som inkluderas i denna export"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
},
@@ -153,7 +186,7 @@
"message": "1.0.0"
},
"app.instance.confirm-delete.admonition-body": {
"message": "All data från din instans kommer att raderas permanent, inklusive dina världar, konfigurationer och allt installerat innehåll."
"message": "All data från din instans kommer permanent raderas, däribland dina världar, konfigurationer samt allt installerat innehåll."
},
"app.instance.confirm-delete.admonition-header": {
"message": "Detta kan inte ogöras"
@@ -275,6 +308,12 @@
"app.modal.update-to-play.update-required-description": {
"message": "En uppdatering krävs för att spela {name}. Vänligen uppdatera till senaste version för att starta spelet."
},
"app.project.install-button.already-installed": {
"message": "Detta projekt är redan installerat"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installera innehåll till instans"
},
"app.settings.developer-mode-enabled": {
"message": "Utvecklarläge aktiverat."
},
@@ -302,9 +341,27 @@
"app.skins.add-button": {
"message": "Lägg till utseende"
},
"app.skins.add-button.drag-and-drop": {
"message": "Dra och släpp"
},
"app.skins.apply-button": {
"message": "Tillämpa"
},
"app.skins.delete-button": {
"message": "Ta bort utseende"
},
"app.skins.delete-modal.description": {
"message": "Du kommer permanent radera det valda utseendet. Detta kan inte ångras."
},
"app.skins.delete-modal.title": {
"message": "Är du säker på att du vill radera utseendet?"
},
"app.skins.dropped-file-error.text": {
"message": "Misslyckades att läsa den släppta filen."
},
"app.skins.dropped-file-error.title": {
"message": "Fel med behandlingen av filen"
},
"app.skins.edit-button": {
"message": "Redigera utseende"
},
@@ -353,6 +410,69 @@
"app.skins.modal.texture-section": {
"message": "Textur"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Ladda upp utseende först!"
},
"app.skins.preview.edit-button": {
"message": "Redigera utseende"
},
"app.skins.previewing-badge": {
"message": "Förhandsvisa"
},
"app.skins.rate-limit.text": {
"message": "Du ändrar ditt utseende för ofta. Mojangs servrar har temporärt blockerat ytterligare förfrågningar. Vänligen vänta en stund innan du försöker igen."
},
"app.skins.rate-limit.title": {
"message": "Sakta ner!"
},
"app.skins.section.chase-the-skies": {
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Standard utseenden"
},
"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 har fått dessa utseende eftersom du donerat till en Modrinth Pride insamling under pride månaden."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Sparade utseenden"
},
"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": "Logga in"
},
"app.skins.sign-in.description": {
"message": "Snälla logga in din Minecraft konto att använda hud hantering förmågor i Modrinth appen."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Exalterad Modrinth bot"
},
"app.skins.sign-in.title": {
"message": "Vänligen logga in"
},
"app.skins.title": {
"message": "Utseende väljare"
},
"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."
},
@@ -372,7 +492,7 @@
"message": "Nedladdning slutförd"
},
"app.update-popup.reload": {
"message": "Ladda om"
"message": "Ladda om för att uppdatera"
},
"app.update-popup.title": {
"message": "Uppdatering tillgänglig"
@@ -572,6 +692,30 @@
"instance.settings.tabs.general.name": {
"message": "Namn"
},
"instance.settings.tabs.general.update-channel": {
"message": "Uppdateringskanal"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Utgåvor, beta och alfa versioner visas som tillgängliga uppdateringar."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Utgåvor och beta versioner visas som tillgängliga uppdateringar."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Utgåva"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Endast utgivna versioner visas som tillgängliga uppdateringar."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Välj uppdateringskanal"
},
"instance.settings.tabs.hooks": {
"message": "Startkrokar"
},
@@ -620,6 +764,24 @@
"instance.settings.tabs.java": {
"message": "Java och minne"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Anpassade miljövariabler"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Anpassade Java-argument"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Anpassad Java-installation"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Anpassad minnestilldelning"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Skriv in miljövariabler..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Skriv in Java-argument..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Miljövariabler"
},
@@ -635,6 +797,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Tilldelat minne"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/sökväg/till/java"
},
"instance.settings.tabs.window": {
"message": "Fönster"
},
@@ -651,7 +816,7 @@
"message": "Höjd"
},
"instance.settings.tabs.window.height.description": {
"message": "Höjden på spel-fönstret vid uppstart."
"message": "Höjden på spelfönstret vid uppstart."
},
"instance.settings.tabs.window.height.enter": {
"message": "Ange höjd..."
@@ -660,7 +825,7 @@
"message": "Bredd"
},
"instance.settings.tabs.window.width.description": {
"message": "Bredden på spel-fönstret vid uppstart."
"message": "Bredden på spelfönstret vid uppstart."
},
"instance.settings.tabs.window.width.enter": {
"message": "Ange bredd..."
@@ -749,7 +914,25 @@
"search.filter.locked.server-loader.title": {
"message": "Loader tillhandahålls av servern"
},
"unknown-pack-warning-modal.body": {
"message": "En fil granskas bara om den laddas upp till Modrinth, oavsett dess filformat (däribland .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Visa inte denna varning igen"
},
"unknown-pack-warning-modal.header": {
"message": "Bekräfta installation"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Installera ändå"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Skadeprogram distribueras ofta via modpaketfiler genom att dela dem på plattformar som Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Vi kunde inte hitta filen på Modrinth. Vi rekommenderar starkt att endast installera filer från källor du litar på."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Okänd filvarning"
}
}
+205 -19
View File
@@ -1,36 +1,45 @@
{
"app.action-bar.downloading-java": {
"message": "ดาวน์โหลดเวอร์ชัน Java {version}"
"message": "กำลังดาวน์โหลด Java {version}"
},
"app.action-bar.downloading-update": {
"message": "กำลังดาวน์โหลดอัปเดต"
},
"app.action-bar.downloads": {
"message": "ดาวน์โหลด"
},
"app.action-bar.hide-more-running-instances": {
"message": "ซ่อนการดำเนินการโปรแกรมเบื้องหลัง"
"message": "ซ่อนอินสแตนซ์ที่ทำงานอยู่เพิ่มเติม"
},
"app.action-bar.make-primary-instance": {
"message": "กำหนดโปรแกรมหลัก"
"message": "กำหนดอินสแตนซ์หลัก"
},
"app.action-bar.no-instances-running": {
"message": "ไม่มีโปรแกรมใดดำเนินการ"
"message": "ไม่มีอินสแตนซ์ที่กำลังทำงาน"
},
"app.action-bar.offline": {
"message": "ออฟไลน์"
},
"app.action-bar.primary-instance": {
"message": "โปรแกรมหลัก"
"message": "อินสแตนซ์หลัก"
},
"app.action-bar.reload-to-update": {
"message": "รีโหลดเพื่ออัปเดต"
},
"app.action-bar.show-more-running-instances": {
"message": "แสดงการดำเนินการโปรแกรมเบื้องหลังเพิ่มเติม"
"message": "แสดงอินสแตนซ์ที่ทำงานอยู่เพิ่มเติม"
},
"app.action-bar.stop-instance": {
"message": "หยุดการทำงานโปรแกรม"
"message": "หยุดอินสแตนซ์"
},
"app.action-bar.update": {
"message": "อัปเดต"
},
"app.action-bar.view-active-downloads": {
"message": "ดูยอดการดาวน์โหลดที่ยังใช้งาน"
"message": "ดูการดาวน์โหลดที่กำลังดำเนินการอยู่"
},
"app.action-bar.view-instance": {
"message": "ดูโปรแกรม"
"message": "ดูอินสแตนซ์"
},
"app.action-bar.view-logs": {
"message": "ดู Log"
@@ -42,7 +51,7 @@
"message": "การเรนเดอร์ขั้นสูง"
},
"app.appearance-settings.color-theme.description": {
"message": "เลือกธีมสีที่คุณชื่นชอบสำหรับ Modrinth บนอุปกรณ์นี้"
"message": "เลือกธีมสีที่คุณต้องการสำหรับ Modrinth App"
},
"app.appearance-settings.color-theme.title": {
"message": "ธีมสี"
@@ -54,22 +63,22 @@
"message": "หน้าหลัก"
},
"app.appearance-settings.default-landing-page.library": {
"message": "รายการ"
"message": "ไลบรารี"
},
"app.appearance-settings.default-landing-page.title": {
"message": "หน้าเริ่มต้น"
},
"app.appearance-settings.hide-nametag.description": {
"message": "ปิดป้ายชื่อบนศีรษะของผู้เล่นในหน้าแสดงสกิน"
"message": "ซ่อนป้ายชื่อบนศีรษะของผู้เล่นในหน้าแสดงสกิน"
},
"app.appearance-settings.hide-nametag.title": {
"message": "ซ่อนป้ายชื่อ"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "การแสดงผลโลกล่าสุดที่ผู้เล่นล่ในส่วน \"กระโดดกลับเข้าไป\" นหน้าหลัก"
"message": "แสดงโลกที่เล่นล่าสุดในส่วน \"เล่นต่อ\" นหน้าหลัก"
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "กระโดดกลับเข้าไปในโลก"
"message": "เล่นต่อ"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "ย่อลันเชอร์เมื่อกระบวนการการรัน Minecraft เริ่มต้นแล้ว"
@@ -78,13 +87,19 @@
"message": "ย่อลันเชอร์"
},
"app.appearance-settings.native-decorations.description": {
"message": "ใช้ระบบกรอบหน้าต่าง (ต้องการการรีสตาร์ทโปรแกรมใหม่)"
"message": "ใช้กรอบหน้าต่างระบบ (ต้องรีสตาร์ทแอป)"
},
"app.appearance-settings.native-decorations.title": {
"message": "ของตกแต่งดั้งเดิม"
},
"app.appearance-settings.select-option": {
"message": "เลือกตัวเลือก"
"message": "เลือกตัวเลือก"
},
"app.appearance-settings.show-play-time.description": {
"message": "แสดงเวลาที่คุณใช้เล่นในแต่ละอินสแตนซ์"
},
"app.appearance-settings.show-play-time.title": {
"message": "แสดงระยะเวลาที่เล่น"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "เปิดใช้งานตัวเลือกในการเลือกย่อหรือขยายแถบด้านข้าง"
@@ -104,6 +119,12 @@
"app.auth-servers.unreachable.header": {
"message": "ไม่สามารถเชื่อมต่อถึงเซิร์ฟเวอร์ได้"
},
"app.browse.add-servers-to-instance": {
"message": "เพิ่มเซิร์ฟเวอร์ลงในอินสแตนซ์"
},
"app.browse.add-to-an-instance": {
"message": "เพิ่มลงในอินสแตนซ์"
},
"app.browse.add-to-instance": {
"message": "เพิ่มลงในโปรแกรม"
},
@@ -116,6 +137,9 @@
"app.browse.already-added": {
"message": "เพิ่มแล้ว"
},
"app.browse.back-to-instance": {
"message": "กลับสู่อินสแตนซ์"
},
"app.browse.discover-content": {
"message": "สำรวจเนื้อหา"
},
@@ -128,9 +152,15 @@
"app.browse.project-type.modpacks": {
"message": "แพ็กม็อด"
},
"app.browse.server-instance-content-warning": {
"message": "การเพิ่มเนื้อหาอาจทำให้ใช้งานร่วมกันไม่ได้เมื่อเข้าร่วมเซิร์ฟเวอร์ เนื้อหาที่เพิ่มเข้ามาจะหายไปเมื่อคุณอัปเดตอินสแตนซ์ของเซิร์ฟเวอร์"
},
"app.browse.server.installing": {
"message": "กำลังติดตั้ง"
},
"app.content-install.no-compatible-versions": {
"message": "ไม่มีเวอร์ชันที่เข้ากันได้ {compatibilityLabel} โปรดเลือกเวอร์ชันที่จะติดตั้ง ไฟล์ที่จำเป็นจะไม่ถูกติดตั้งโดยอัตโนมัติ"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -260,6 +290,9 @@
"app.modal.install-to-play.install-button": {
"message": "ติดตั้ง"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# ม็อด}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "มอดแพ็กที่จำเป็น"
},
@@ -284,6 +317,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "จำเป็นต้องอัปเดตเพื่อเล่น {name} กรุณาอัปเดตเป็นเวอร์ชันล่าสุดเพื่อเปิดเกม"
},
"app.project.install-button.already-installed": {
"message": "โปรเจคนี้ถูกติดตั้งแล้ว"
},
"app.project.install-context.back-to-browse": {
"message": "กลับไปที่หน้าค้นหา"
},
"app.project.install-context.install-content-to-instance": {
"message": "ติดตั้งเนื้อหาลงสู่อินสแตนซ์"
},
"app.settings.developer-mode-enabled": {
"message": "โหมดนักพัฒนาเปิดอยู่"
},
@@ -308,6 +350,150 @@
"app.settings.tabs.resource-management": {
"message": "การจัดการทรัพยากร"
},
"app.skins.add-button": {
"message": "เพิ่มสกิน"
},
"app.skins.add-button.drag-and-drop": {
"message": "ลากและวาง"
},
"app.skins.apply-button": {
"message": "นำไปใช้"
},
"app.skins.delete-button": {
"message": "ลบสกิน"
},
"app.skins.delete-modal.description": {
"message": "การดำเนินการนี้จะลบสกินที่เลือกอย่างถาวร การกระทำนี้ไม่สามารถย้อนกลับได้"
},
"app.skins.delete-modal.title": {
"message": "คุณแน่ใจหรือว่าต้องการที่จะลบสกินนี้?"
},
"app.skins.dropped-file-error.text": {
"message": "ไม่สามารถอ่านไฟล์ที่ถูกลากและวางได้"
},
"app.skins.dropped-file-error.title": {
"message": "ข้อผิดพลาดในการประมวลผลไฟล์"
},
"app.skins.edit-button": {
"message": "แก้ไขสกิน"
},
"app.skins.modal.add-skin-button": {
"message": "เพิ่มสกิน"
},
"app.skins.modal.add-title": {
"message": "การเพิ่มสกิน"
},
"app.skins.modal.arm-style-section": {
"message": "สไตล์แขน"
},
"app.skins.modal.arm-style-slim": {
"message": "เพรียว"
},
"app.skins.modal.arm-style-wide": {
"message": "กว้าง"
},
"app.skins.modal.cape-fallback-name": {
"message": "ผ้าคลุม"
},
"app.skins.modal.cape-section": {
"message": "ผ้าคลุม"
},
"app.skins.modal.edit-title": {
"message": "แก้ไขสกิน"
},
"app.skins.modal.make-edit-first-tooltip": {
"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.text": {
"message": "คุณได้เปลี่ยนสกินบ่อยเกินไป เซิร์ฟเวอร์ของ Mojang ได้บล็อกคำขอเพิ่มเติมชั่วคราว โปรดรอสักครู่ก่อนลองอีกครั้ง"
},
"app.skins.rate-limit.title": {
"message": "ลดความถี่ลง!"
},
"app.skins.reorder-error.text": {
"message": "ไม่สามารถบันทึกลำดับสกินของคุณได้"
},
"app.skins.reorder-error.title": {
"message": "ไม่สามารถจัดเรียงสกินได้"
},
"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": "สกินเริ่มต้น"
},
"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 ในช่วงเดือน Pride Month"
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "สกินที่บันทึกไว้"
},
"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": "ลงชื่อเข้าใช้"
},
"app.skins.sign-in.description": {
"message": "โปรดลงชื่อเข้าใช้บัญชี Minecraft ของคุณเพื่อใช้งานฟีเจอร์จัดการสกินของแอป Modrinth"
},
"app.skins.sign-in.rinthbot-alt": {
"message": "บอท Modrinth ที่กำลังตื่นเต้น"
},
"app.skins.sign-in.title": {
"message": "โปรดลงชื่อเข้าใช้"
},
"app.update-popup.body.download-complete": {
"message": "ดาวน์โหลด Modrinth v{version} สำเร็จแล้ว เปิดโปรแกรมใหม่อีกครั้งเพื่ออัปเดตตอนนี้ หรือจะรออัปเดตอัตโนมัติ ซึ่งจะเกิดขึ้นเมื่อคุณกดปิดโปรแกรม Modrinth"
},
@@ -326,9 +512,6 @@
"app.update-popup.download-complete": {
"message": "การดาวน์โหลดเสร็จสมบูรณ์"
},
"app.update-popup.reload": {
"message": "โหลดใหม่"
},
"app.update-popup.title": {
"message": "มีการอัปเดตใหม่"
},
@@ -359,6 +542,9 @@
"friends.action.add-friend": {
"message": "เพิ่มเพื่อน"
},
"friends.action.view-friend-requests": {
"message": "คำขอเป็นเพื่อน {count} {count, plural, other {คน}}"
},
"friends.add-friend.submit": {
"message": "ส่งคำขอเป็นเพื่อน"
},
+45 -3
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Java {version} indiriliyor"
},
"app.action-bar.downloading-update": {
"message": "Güncelleme indiriliyor"
},
"app.action-bar.downloads": {
"message": "İndirmeler"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Birincil kurulum"
},
"app.action-bar.reload-to-update": {
"message": "Güncellemek için yeniden yükle"
},
"app.action-bar.show-more-running-instances": {
"message": "Daha fazla çalışan kurulum göster"
},
"app.action-bar.stop-instance": {
"message": "Kurulumu durdur"
},
"app.action-bar.update": {
"message": "Güncelleme"
},
"app.action-bar.view-active-downloads": {
"message": "Şu anda indirilienleri göster"
},
@@ -45,7 +54,7 @@
"message": "Modrinth App için tercih ettiğiniz temayı seçin."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema"
"message": "Tema Rengi"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Launcher açıldığında varsayılan olarak açılacak sayfayı seçin."
@@ -135,7 +144,7 @@
"message": "İçerik keşfet"
},
"app.browse.discover-servers": {
"message": "Sunucu keşfet"
"message": "Sunucuları keşfet"
},
"app.browse.hide-added-servers": {
"message": "Zaten eklenmiş sunucuları gizle"
@@ -428,6 +437,12 @@
"app.skins.rate-limit.title": {
"message": "Yavaşla!"
},
"app.skins.reorder-error.text": {
"message": "Skinini kaydedemedik."
},
"app.skins.reorder-error.title": {
"message": "Skinler sıralanamadı"
},
"app.skins.section.builders-and-biomes": {
"message": "İnşaatçılar & Biyomlar"
},
@@ -501,7 +516,7 @@
"message": "İndirme tamamlandı"
},
"app.update-popup.reload": {
"message": "Yeniden başlat"
"message": "Güncellemek için yeniden yükle"
},
"app.update-popup.title": {
"message": "Güncelleme mevcut"
@@ -533,6 +548,9 @@
"friends.action.add-friend": {
"message": "Bir arkadaş ekle"
},
"friends.action.view-friend-requests": {
"message": "{count} arkadaşlık isteği"
},
"friends.add-friend.submit": {
"message": "Arkadaşlık isteği gönder"
},
@@ -698,6 +716,30 @@
"instance.settings.tabs.general.name": {
"message": "Ad"
},
"instance.settings.tabs.general.update-channel": {
"message": "Güncelleme kanalı"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Release, beta ve alpha versiyonları mevcut güncellemeler olarak gösterilecek."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Release ve beta versiyonları mevcut güncellemeler olarak gösterilecek."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Yayın"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Yalnızca release versiyonları mevcut güncellemeler olarak gösterilecek."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Güncelleme kanalını seç"
},
"instance.settings.tabs.hooks": {
"message": "Başlatma kancaları"
},
+64 -16
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Завантаження Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Завантаження оновлення"
},
"app.action-bar.downloads": {
"message": "Завантаження"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Основний профіль"
},
"app.action-bar.reload-to-update": {
"message": "Перезавантаження для оновлення"
},
"app.action-bar.show-more-running-instances": {
"message": "Показати більше запущених профілів"
},
"app.action-bar.stop-instance": {
"message": "Зупинити профіль"
},
"app.action-bar.update": {
"message": "Оновлення"
},
"app.action-bar.view-active-downloads": {
"message": "Переглянути активні завантаження"
},
@@ -149,6 +158,9 @@
"app.browse.server.installing": {
"message": "Установлення"
},
"app.content-install.no-compatible-versions": {
"message": "Жодна доступна версія не сумісна з {compatibilityLabel}. Виберіть версію, щоб усе одно її встановити. Залежності не встановлюватимуться автоматично."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -195,7 +207,7 @@
"message": "Видалити профіль"
},
"app.instance.modpack-already-installed.body": {
"message": "Ця збірка вже встановлена в профілі <bold>«{instanceName}»</bold>. Ви впевнені, що хочете її скопіювати?"
"message": "Ця збірка вже встановлена в профілі <bold>«{instanceName}»</bold>. Дійсно бажаєте її клонувати?"
},
"app.instance.modpack-already-installed.create": {
"message": "Створити"
@@ -269,6 +281,9 @@
"app.modal.install-to-play.install-button": {
"message": "Установити"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# мод} few {# мода} other {# модів}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Потрібна збірка"
},
@@ -285,13 +300,13 @@
"message": "Дивитися вміст"
},
"app.modal.update-to-play.header": {
"message": "Оновлення перед грою"
"message": "Оновлення для гри"
},
"app.modal.update-to-play.update-required": {
"message": "Необхідне оновлення"
},
"app.modal.update-to-play.update-required-description": {
"message": "«{name}» потребує оновлення, щоб грати. Будь ласка, оновіть гру до останньої версії, щоб запустити її."
"message": "«{name}» потребує оновлення, щоб грати. Будь ласка, оновіть до останньої версії, щоб запустити гру."
},
"app.project.install-button.already-installed": {
"message": "Проєкт уже встановлено"
@@ -330,7 +345,7 @@
"message": "Додати скін"
},
"app.skins.add-button.drag-and-drop": {
"message": "Перетягніть і вставте"
"message": "Перетягніть сюди"
},
"app.skins.apply-button": {
"message": "Застосувати"
@@ -342,7 +357,7 @@
"message": "Це видалить вибраний скін назавжди. Ця дія не може бути скасована."
},
"app.skins.delete-modal.title": {
"message": "Чи ви впевнені що хочете видалити цей скін?"
"message": "Ви дійсно бажаєте видалити цей скін?"
},
"app.skins.dropped-file-error.text": {
"message": "Неможливо відкрити вибраний файл."
@@ -378,19 +393,19 @@
"message": "Редагування скіна"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Відредагуйте скін для початку!"
"message": "Спершу відредагуйте скін!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Без плаща"
},
"app.skins.modal.none-cape-option": {
"message": "Без"
"message": "Немає"
},
"app.skins.modal.replace-texture-button": {
"message": "Замінити текстуру"
},
"app.skins.modal.save-skin-button": {
"message": "Зберегти вигляд"
"message": "Зберегти скін"
},
"app.skins.modal.saving-tooltip": {
"message": "Збереження…"
@@ -399,7 +414,7 @@
"message": "Текстура"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Завантажте скін для початку!"
"message": "Спершу завантажте скін!"
},
"app.skins.preview.edit-button": {
"message": "Редагувати скін"
@@ -408,10 +423,16 @@
"message": "Попередній перегляд"
},
"app.skins.rate-limit.text": {
"message": "Ви змінюєте свій скін дуже часто. Сервери Mojang тимчасово заблокували подальші запити. Зачекайте трохи перш ніж спробувати знову."
"message": "Ви змінюєте скін занадто часто. Сервери Mojang тимчасово заблокували подальші запити. Зачекайте трохи, перш ніж спробувати знову."
},
"app.skins.rate-limit.title": {
"message": "Шановний!"
"message": "Повільніше!"
},
"app.skins.reorder-error.text": {
"message": "Ваше замовлення скінів не вдалося зберегти."
},
"app.skins.reorder-error.title": {
"message": "Не вдалося змінити порядок скінів"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
@@ -468,13 +489,13 @@
"message": "Вибір скіну"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} завантажено. Перезапустіть зараз, щоб оновити його, або це відбудеться автоматично після закриття Modrinth App."
"message": "Modrinth App v{version} завантажено. Перезапустіть, щоб оновити зараз, або це відбудеться автоматично після закриття застосунку."
},
"app.update-popup.body.linux": {
"message": "Modrinth App v{version} вже доступно. Скористайтеся вашим менеджером пакетів, щоб отримати найновіші функції та виправлення!"
"message": "Modrinth App v{version} вже доступно. Скористайтеся менеджером пакетів, щоб отримати найновіші функції та виправлення!"
},
"app.update-popup.body.metered": {
"message": "Modrinth App v{version} доступний для завантаження! Оскільки ви на лімітному з’єднанні, ми не завантажили його автоматично."
"message": "Modrinth App v{version} вже доступно! Оскільки ви використовуєте лімітне підключення, ми не завантажили його автоматично."
},
"app.update-popup.changelog": {
"message": "Журнал змін"
@@ -486,7 +507,7 @@
"message": "Завантаження завершено"
},
"app.update-popup.reload": {
"message": "Перезавантажити"
"message": "Перезавантаження для оновлення"
},
"app.update-popup.title": {
"message": "Доступне оновлення"
@@ -510,7 +531,7 @@
"message": "Ще не зіграно"
},
"app.world.world-item.offline": {
"message": "Поза мережею"
"message": "Не в мережі"
},
"app.world.world-item.players-online": {
"message": "{count} у мережі"
@@ -518,6 +539,9 @@
"friends.action.add-friend": {
"message": "Додати друга"
},
"friends.action.view-friend-requests": {
"message": "{count} запит{count, plural, one { у} few {и в} many {ів у} other {у в}} друзі"
},
"friends.add-friend.submit": {
"message": "Надіслати запит у друзі"
},
@@ -683,6 +707,30 @@
"instance.settings.tabs.general.name": {
"message": "Назва"
},
"instance.settings.tabs.general.update-channel": {
"message": "Випуск оновлення"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Альфа"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Випуски, бета- та альфа-версії будуть показуватися як доступні оновлення."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Бета"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Випуски та бета-версії будуть показуватися як доступні оновлення."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Випуск"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Тільки випуски версії будуть показуватися як доступні оновлення."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Оберіть випуск оновлення"
},
"instance.settings.tabs.hooks": {
"message": "Гуки запуску"
},
+49 -1
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "Đang tải xuống Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Đang tải xuống bản cập nhật"
},
"app.action-bar.downloads": {
"message": "Tải xuống"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "Bản instance chính"
},
"app.action-bar.reload-to-update": {
"message": "Tải lại để cập nhật"
},
"app.action-bar.show-more-running-instances": {
"message": "Hiện thêm các phiên bản đang chạy"
},
"app.action-bar.stop-instance": {
"message": "Dừng instance"
},
"app.action-bar.update": {
"message": "Cập nhật"
},
"app.action-bar.view-active-downloads": {
"message": "Xem các lượt tải xuống"
},
@@ -149,6 +158,9 @@
"app.browse.server.installing": {
"message": "Đang cài đặt"
},
"app.content-install.no-compatible-versions": {
"message": "Không có phiên bản nào phù hợp với {compatibilityLabel}. Nếu vẫn chọn một phiên bản để cài đặt, các gói phụ thuộc sẽ không được tải tự động."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -278,6 +290,9 @@
"app.modal.install-to-play.install-button": {
"message": "Tải xuống"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# mod}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Yêu cầu modpack"
},
@@ -422,6 +437,12 @@
"app.skins.rate-limit.title": {
"message": "Bình tĩnh lại!"
},
"app.skins.reorder-error.text": {
"message": "Không thể lưu thứ tự skin của bạn."
},
"app.skins.reorder-error.title": {
"message": "Sắp xếp lại skin thất bại"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
},
@@ -495,7 +516,7 @@
"message": "Cài đặt thành công"
},
"app.update-popup.reload": {
"message": "Khởi động lại"
"message": "Tải lại để cập nhật"
},
"app.update-popup.title": {
"message": "Cập nhật mới hiện đang khả dụng"
@@ -527,6 +548,9 @@
"friends.action.add-friend": {
"message": "Thêm một người bạn"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, other {yêu cầu kết bạn}}"
},
"friends.add-friend.submit": {
"message": "Gửi yêu cầu kết bạn"
},
@@ -692,6 +716,30 @@
"instance.settings.tabs.general.name": {
"message": "Tên"
},
"instance.settings.tabs.general.update-channel": {
"message": "Kênh cập nhật"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Sơ khai"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Các phiên bản chính thức, thử nghiệm và sơ khai sẽ hiển thị trong danh sách cập nhật có sẵn."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Thử nghiệm"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Các phiên bản chính thức và thử nghiệm sẽ hiển thị trong danh sách cập nhật có sẵn."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Chính thức"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Chỉ phiên bản chính thức sẽ hiển thị trong danh sách cập nhật có sẵn."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Chọn kênh cập nhật"
},
"instance.settings.tabs.hooks": {
"message": "Khởi chạy Hooks"
},
+49 -4
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "正在下载 Java {version}"
},
"app.action-bar.downloading-update": {
"message": "下载更新中"
},
"app.action-bar.downloads": {
"message": "下载"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "主实例"
},
"app.action-bar.reload-to-update": {
"message": "重新加载以更新"
},
"app.action-bar.show-more-running-instances": {
"message": "显示更多运行中的实例"
},
"app.action-bar.stop-instance": {
"message": "停止实例"
},
"app.action-bar.update": {
"message": "更新"
},
"app.action-bar.view-active-downloads": {
"message": "查看正在下载内容"
},
@@ -63,7 +72,7 @@
"message": "在皮肤页面中禁用你玩家头顶上的名牌。"
},
"app.appearance-settings.hide-nametag.title": {
"message": "隐藏名称标签"
"message": "隐藏名"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "在主页的“快速回到”部分包含最近的世界。"
@@ -111,7 +120,7 @@
"message": "无法连接到身份验证服务器"
},
"app.browse.add-servers-to-instance": {
"message": "将服务器添加到实例"
"message": "正在添加服务器到此实例"
},
"app.browse.add-to-an-instance": {
"message": "添加到实例"
@@ -144,7 +153,7 @@
"message": "整合包"
},
"app.browse.server-instance-content-warning": {
"message": "加入服务器时,添加内容可能会破坏兼容性。当更新服务器实例内容时任何添加的内容也将丢失。"
"message": "加入服务器时,添加内容可能会破坏兼容性。当更新服务器实例内容时任何添加的内容也将丢失。"
},
"app.browse.server.installing": {
"message": "安装中"
@@ -272,6 +281,9 @@
"app.modal.install-to-play.install-button": {
"message": "安装"
},
"app.modal.install-to-play.mod-count": {
"message": "{count} 个模组"
},
"app.modal.install-to-play.required-modpack": {
"message": "需求整合包"
},
@@ -416,6 +428,12 @@
"app.skins.rate-limit.title": {
"message": "慢点!"
},
"app.skins.reorder-error.text": {
"message": "未能保存皮肤顺序。"
},
"app.skins.reorder-error.title": {
"message": "皮肤排序失败"
},
"app.skins.section.builders-and-biomes": {
"message": "建造者与生物群系"
},
@@ -489,7 +507,7 @@
"message": "下载完成"
},
"app.update-popup.reload": {
"message": "重新启动"
"message": "重新加载以更新"
},
"app.update-popup.title": {
"message": "有可用的更新"
@@ -521,6 +539,9 @@
"friends.action.add-friend": {
"message": "添加好友"
},
"friends.action.view-friend-requests": {
"message": "{count} 个好友请求"
},
"friends.add-friend.submit": {
"message": "发送好友请求"
},
@@ -686,6 +707,30 @@
"instance.settings.tabs.general.name": {
"message": "名称"
},
"instance.settings.tabs.general.update-channel": {
"message": "更新渠道"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha 测试版"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "正式版,beta 和 alpha 测试版会被显示为可用更新。"
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta 测试版"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "正式版和 beta 测试版会被显示为可用更新。"
},
"instance.settings.tabs.general.update-channel.release": {
"message": "正式版"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "只有正式版会被显示为可用更新。"
},
"instance.settings.tabs.general.update-channel.select": {
"message": "选择更新渠道"
},
"instance.settings.tabs.hooks": {
"message": "启动 Hooks"
},
+142 -31
View File
@@ -2,6 +2,9 @@
"app.action-bar.downloading-java": {
"message": "正在下載 Java {version}"
},
"app.action-bar.downloading-update": {
"message": "正在下載更新"
},
"app.action-bar.downloads": {
"message": "下載"
},
@@ -20,12 +23,18 @@
"app.action-bar.primary-instance": {
"message": "主要實例"
},
"app.action-bar.reload-to-update": {
"message": "重新載入即可更新"
},
"app.action-bar.show-more-running-instances": {
"message": "顯示更多執行中的實例"
},
"app.action-bar.stop-instance": {
"message": "停止實例"
},
"app.action-bar.update": {
"message": "更新"
},
"app.action-bar.view-active-downloads": {
"message": "查看正在下載的項目"
},
@@ -33,10 +42,10 @@
"message": "查看實例"
},
"app.action-bar.view-logs": {
"message": "查看紀錄"
"message": "查看紀錄"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "啟用進階繪製(如模糊效果)若無硬體加速可能會導致效能問題。"
"message": "啟用進階繪製(如模糊效果)若無硬體加速可能會導致效能問題。"
},
"app.appearance-settings.advanced-rendering.title": {
"message": "進階繪製"
@@ -78,19 +87,19 @@
"message": "最小化啟動器"
},
"app.appearance-settings.native-decorations.description": {
"message": "使用系統視窗外框(需要重新啟動應用程式)。"
"message": "使用系統內建的視窗外框(需要重應用程式)。"
},
"app.appearance-settings.native-decorations.title": {
"message": "原生裝飾"
"message": "原生視窗"
},
"app.appearance-settings.select-option": {
"message": "選擇選項"
},
"app.appearance-settings.show-play-time.description": {
"message": "顯示在該實例的遊玩時"
"message": "顯示在該實例的遊玩時數。"
},
"app.appearance-settings.show-play-time.title": {
"message": "顯示遊玩時"
"message": "顯示遊玩時"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "啟用切換側邊欄的功能。"
@@ -149,14 +158,17 @@
"app.browse.server.installing": {
"message": "安裝中"
},
"app.content-install.no-compatible-versions": {
"message": "沒有符合 {compatibilityLabel} 的可用版本。請選擇一個版本強制安裝。不會自動安裝相依模組。"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "正在安裝模組包……"
"message": "正在安裝模組包..."
},
"app.export-modal.description-placeholder": {
"message": "輸入模組包描述……"
"message": "輸入模組包描述..."
},
"app.export-modal.export-button": {
"message": "匯出"
@@ -255,7 +267,7 @@
"message": "確定要移除「{name}」嗎?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "搜尋 {count} 個世界……"
"message": "搜尋 {count} 個世界..."
},
"app.instance.worlds.this-server": {
"message": "這個伺服器"
@@ -269,6 +281,9 @@
"app.modal.install-to-play.install-button": {
"message": "安裝"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# 個模組}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "所需模組包"
},
@@ -327,25 +342,46 @@
"message": "資源管理"
},
"app.skins.add-button": {
"message": "新增皮膚"
"message": "新增外觀"
},
"app.skins.add-button.drag-and-drop": {
"message": "拖曳"
},
"app.skins.apply-button": {
"message": "確認"
"message": "套用"
},
"app.skins.delete-button": {
"message": "刪除皮膚"
"message": "刪除外觀"
},
"app.skins.delete-modal.description": {
"message": "這會永久刪除所選的外觀,這個動作無法復原。"
},
"app.skins.delete-modal.title": {
"message": "你確定要刪除這個皮膚嗎"
"message": "你確定要刪除這個外觀嗎?"
},
"app.skins.dropped-file-error.text": {
"message": "無法讀取拖曳的檔案。"
},
"app.skins.dropped-file-error.title": {
"message": "處理檔案時發生錯誤"
},
"app.skins.edit-button": {
"message": "編輯皮膚"
"message": "編輯外觀"
},
"app.skins.modal.add-skin-button": {
"message": "新增皮膚"
"message": "新增外觀"
},
"app.skins.modal.add-title": {
"message": "正在新增皮膚"
"message": "新增外觀"
},
"app.skins.modal.arm-style-section": {
"message": "手臂身形"
},
"app.skins.modal.arm-style-slim": {
"message": "苗條"
},
"app.skins.modal.arm-style-wide": {
"message": "寬"
},
"app.skins.modal.cape-fallback-name": {
"message": "披風"
@@ -354,7 +390,10 @@
"message": "披風"
},
"app.skins.modal.edit-title": {
"message": "編輯皮膚"
"message": "編輯外觀"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "請先對外觀進行編輯!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "無披風"
@@ -363,46 +402,91 @@
"message": "無"
},
"app.skins.modal.replace-texture-button": {
"message": "覆蓋材質"
"message": "取代材質"
},
"app.skins.modal.save-skin-button": {
"message": "儲存皮膚"
"message": "儲存外觀"
},
"app.skins.modal.saving-tooltip": {
"message": "正在儲存"
"message": "正在儲存..."
},
"app.skins.modal.texture-section": {
"message": "材質"
"message": "材質"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "馬上新增一個皮膚"
"message": "請先上傳外觀檔案!"
},
"app.skins.preview.edit-button": {
"message": "編輯皮膚"
"message": "編輯外觀"
},
"app.skins.previewing-badge": {
"message": "正在預覽"
"message": "預覽"
},
"app.skins.rate-limit.text": {
"message": "你變更外觀的頻率太過頻繁。Mojang 伺服器已暫時封鎖後續請求,請稍等片刻,然後再試一次。"
},
"app.skins.rate-limit.title": {
"message": "慢一點"
"message": "慢一點"
},
"app.skins.reorder-error.text": {
"message": "無法儲存你的外觀排序。"
},
"app.skins.reorder-error.title": {
"message": "重新排序外觀失敗"
},
"app.skins.section.builders-and-biomes": {
"message": "建造者&生態域"
},
"app.skins.section.chase-the-skies": {
"message": "追逐天空"
},
"app.skins.section.default-skins": {
"message": "預設皮膚"
"message": "預設外觀"
},
"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": "群騎紛爭"
},
"app.skins.section.saved-skins": {
"message": "皮膚已儲存"
"message": "已儲存外觀"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "銅器時代"
},
"app.skins.section.the-garden-awakens": {
"message": "蒼園覺醒"
},
"app.skins.section.tiny-takeover": {
"message": "小鬼當家"
},
"app.skins.sign-in.button": {
"message": "登入"
},
"app.skins.sign-in.description": {
"message": "請登入你的 Minecraft 帳號以使用 Modrinth App 的外觀管理功能。"
},
"app.skins.sign-in.rinthbot-alt": {
"message": "興奮的 Modrinth 機器人"
},
"app.skins.sign-in.title": {
"message": "請登入"
},
"app.skins.title": {
"message": "皮膚選擇"
"message": "外觀選擇工具"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} 已完成下載!立即重新載入以更新,或在關閉 Modrinth App 時自動更新。"
@@ -423,7 +507,7 @@
"message": "下載完成"
},
"app.update-popup.reload": {
"message": "重新載入"
"message": "重新載入即可更新"
},
"app.update-popup.title": {
"message": "有可用的更新"
@@ -455,6 +539,9 @@
"friends.action.add-friend": {
"message": "新增好友"
},
"friends.action.view-friend-requests": {
"message": "{count} 個好友{count, plural, other {邀請}}"
},
"friends.add-friend.submit": {
"message": "送出好友邀請"
},
@@ -620,6 +707,30 @@
"instance.settings.tabs.general.name": {
"message": "名稱"
},
"instance.settings.tabs.general.update-channel": {
"message": "更新通道"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha 版"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "相容的正式版、Beta 版和 Alpha 版本都會顯示在可用更新中。"
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta 版"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "相容的正式版和 Beta 版本都會顯示在可用更新中。"
},
"instance.settings.tabs.general.update-channel.release": {
"message": "正式版"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "僅有正式版會顯示在可用更新中。"
},
"instance.settings.tabs.general.update-channel.select": {
"message": "選擇更新通道"
},
"instance.settings.tabs.hooks": {
"message": "啟動掛勾"
},
@@ -681,10 +792,10 @@
"message": "自訂記憶體分配"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "輸入環境變數……"
"message": "輸入環境變數..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "輸入 Java 參數……"
"message": "輸入 Java 參數..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "環境變數"
+72 -22
View File
@@ -29,7 +29,7 @@ import {
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import type { Ref } from 'vue'
import { computed, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import type { LocationQuery } from 'vue-router'
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
@@ -41,6 +41,7 @@ import {
get_search_results_v3,
get_version_many,
} from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events.js'
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata'
import {
get as getInstance,
@@ -181,6 +182,28 @@ watchServerContextChanges()
await initInstanceContext()
async function refreshInstalledProjectIds() {
if (!route.query.i) return
if (route.query.from === 'worlds') {
const worlds = await get_profile_worlds(route.query.i as string).catch(handleError)
if (!worlds) return
const serverProjectIds = worlds
.filter((w) => w.type === 'server' && 'project_id' in w && w.project_id)
.map((w) => (w as { project_id: string }).project_id)
debugLog('installedServerProjectIds loaded', { count: serverProjectIds.length })
installedProjectIds.value = serverProjectIds
return
}
const ids = await getInstalledProjectIds(route.query.i as string).catch(handleError)
if (!ids) return
debugLog('installedProjectIds loaded', { count: ids.length })
installedProjectIds.value = ids
}
async function initInstanceContext() {
debugLog('initInstanceContext', {
queryI: route.query.i,
@@ -199,24 +222,7 @@ async function initInstanceContext() {
gameVersion: instance.value?.game_version,
})
if (route.query.from === 'worlds') {
get_profile_worlds(route.query.i as string)
.then((worlds) => {
const serverProjectIds = worlds
.filter((w) => w.type === 'server' && 'project_id' in w && w.project_id)
.map((w) => (w as { project_id: string }).project_id)
debugLog('installedServerProjectIds loaded', { count: serverProjectIds.length })
installedProjectIds.value = serverProjectIds
})
.catch(handleError)
} else {
getInstalledProjectIds(route.query.i as string)
.then((ids) => {
debugLog('installedProjectIds loaded', { count: ids?.length })
installedProjectIds.value = ids
})
.catch(handleError)
}
await refreshInstalledProjectIds()
if (instance.value?.linked_data?.project_id) {
debugLog('checking linked project for server status', instance.value.linked_data.project_id)
@@ -805,10 +811,10 @@ function getCardActions(
selectedInstall.versionId,
instance.value ? instance.value.path : null,
'SearchCard',
(versionId) => {
(versionId, installedProjectIds) => {
setProjectInstalling(projectResult.project_id, false)
if (versionId) {
onSearchResultInstalled(projectResult.project_id)
onSearchResultsInstalled(installedProjectIds ?? [projectResult.project_id])
}
},
(profile) => {
@@ -834,7 +840,19 @@ function onSearchResultInstalled(id: string) {
markServerProjectInstalled(id)
return
}
newlyInstalled.value.push(id)
if (!newlyInstalled.value.includes(id)) {
newlyInstalled.value = [...newlyInstalled.value, id]
}
}
function onSearchResultsInstalled(ids: string[]) {
if (isServerContext.value) {
for (const id of ids) {
markServerProjectInstalled(id)
}
return
}
newlyInstalled.value = Array.from(new Set([...newlyInstalled.value, ...ids]))
}
async function search(requestParams: string) {
@@ -966,6 +984,38 @@ if (instance.value?.game_version) {
await searchState.refreshSearch()
type UnlistenFn = () => void
let isUnmounted = false
let unlistenProfiles: UnlistenFn | null = null
onMounted(() => {
profile_listener(async (event: { event: string; profile_path_id: string }) => {
if (
instance.value &&
event.profile_path_id === instance.value.path &&
event.event === 'synced'
) {
await refreshInstalledProjectIds()
await searchState.refreshSearch()
}
})
.then((unlisten) => {
if (isUnmounted) {
unlisten()
return
}
unlistenProfiles = unlisten
})
.catch(handleError)
})
onUnmounted(() => {
isUnmounted = true
unlistenProfiles?.()
})
function getProjectBrowseQuery() {
if (!installContext.value) return undefined
return {
+92 -13
View File
@@ -90,6 +90,7 @@ import {
useVIntl,
versionChangesGameVersion,
} from '@modrinth/ui'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { open } from '@tauri-apps/plugin-dialog'
import { openUrl } from '@tauri-apps/plugin-opener'
@@ -99,7 +100,7 @@ import { useRouter } from 'vue-router'
import ExportModal from '@/components/ui/ExportModal.vue'
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { get_project_versions, get_version } from '@/helpers/cache.js'
import { get_project_versions, get_version, get_version_many } from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events.js'
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
import {
@@ -153,6 +154,7 @@ const { formatMessage } = useVIntl()
const { handleError, addNotification } = injectNotificationManager()
const { installingItems } = injectContentInstall()
const router = useRouter()
const queryClient = useQueryClient()
const debug = useDebugLogger('Mods:ContentUpdate')
const props = defineProps<{
@@ -212,6 +214,13 @@ const contentUpdaterModal = ref<InstanceType<typeof ContentUpdaterModal> | null>
const modpackContentModal = ref<InstanceType<typeof ModpackContentModal> | null>()
const modpackUpdateConfirmModal = ref<InstanceType<typeof ConfirmModpackUpdateModal> | null>()
const modpackContentQueryKey = computed(() => ['linkedModpackContent', props.instance.path])
const modpackContentQuery = useQuery({
queryKey: modpackContentQueryKey,
queryFn: () => get_linked_modpack_content(props.instance.path),
enabled: computed(() => !!props.instance?.path && !!props.instance?.linked_data),
})
// TODO: Extract content operation and updater modal state into composables; this page currently owns file mutations, dependency installs, busy flags, and version selection flow.
const updatingProject = ref<ContentItem | null>(null)
const updatingProjectVersions = ref<Labrinth.Versions.v2.Version[]>([])
@@ -451,6 +460,63 @@ async function removeMod(mod: ContentItem) {
}
}
function isBreakingDependency(dependency: Labrinth.Versions.v2.Dependency) {
return dependency.dependency_type === 'required' || dependency.dependency_type === 'embedded'
}
function dependencyTargetsItem(dependency: Labrinth.Versions.v2.Dependency, item: ContentItem) {
return (
(!!dependency.project_id && dependency.project_id === item.project?.id) ||
('version_id' in dependency &&
!!dependency.version_id &&
dependency.version_id === item.version?.id)
)
}
async function getDeleteDependencyWarning(items: ContentItem[]) {
if (props.isServerInstance) return null
const deletingIds = new Set(items.map(getContentItemId))
const remainingItems = projects.value.filter((item) => !deletingIds.has(getContentItemId(item)))
const versionIds = [
...new Set(remainingItems.map((item) => item.version?.id).filter((id): id is string => !!id)),
]
if (versionIds.length === 0) return null
const versions = (await get_version_many(versionIds).catch((err) => {
handleError(err as Error)
return null
})) as Labrinth.Versions.v2.Version[] | null
if (!versions) return null
const versionsById = new Map(versions.map((version) => [version.id, version]))
const dependents = remainingItems
.map((candidate) => {
const version = candidate.version?.id ? versionsById.get(candidate.version.id) : null
if (!version) return null
const dependencies = items.filter((item) => {
if (!item.project?.id && !item.version?.id) return false
return version.dependencies?.some(
(dependency) =>
isBreakingDependency(dependency) && dependencyTargetsItem(dependency, item),
)
})
return dependencies.length > 0 ? { item: candidate, dependencies } : null
})
.filter(
(dependent): dependent is { item: ContentItem; dependencies: ContentItem[] } =>
dependent !== null,
)
return dependents.length > 0 ? { items, dependents } : null
}
async function updateProject(mod: ContentItem) {
if (!mod.file_path) return
const operation = beginContentOperation(mod)
@@ -481,6 +547,7 @@ async function updateProject(mod: ContentItem) {
})
} catch (err) {
handleError(err as Error)
throw err
} finally {
await refreshContentState('must_revalidate')
finishContentOperation(mod, operation)
@@ -682,13 +749,19 @@ async function handleModpackContentBulkToggle(items: ContentItem[]) {
async function handleModpackContent() {
if (!props.instance?.path) return
if (modpackContentQuery.data.value !== undefined) {
modpackContentModal.value?.show(modpackContentQuery.data.value)
return
}
modpackContentModal.value?.showLoading()
const contentItems = await get_linked_modpack_content(props.instance.path).catch(handleError)
const { data, error } = await modpackContentQuery.refetch()
if (contentItems) {
modpackContentModal.value?.show(contentItems)
if (data !== undefined) {
modpackContentModal.value?.show(data)
} else {
if (error) handleError(error)
modpackContentModal.value?.hide()
}
}
@@ -696,9 +769,12 @@ async function handleModpackContent() {
async function refreshModpackContentItems(cacheBehaviour?: CacheBehaviour) {
if (!props.instance?.path) return
const contentItems = await get_linked_modpack_content(props.instance.path, cacheBehaviour).catch(
handleError,
)
const contentItems = await queryClient
.fetchQuery({
queryKey: modpackContentQueryKey.value,
queryFn: () => get_linked_modpack_content(props.instance.path, cacheBehaviour),
})
.catch(handleError)
if (contentItems) {
modpackContentModal.value?.setItems(contentItems)
@@ -885,13 +961,15 @@ async function handleModalUpdate(
} else if (updatingProject.value) {
const mod = updatingProject.value
if (mod.has_update && mod.update_version_id === selectedVersion.id) {
await updateProject(mod)
} else {
await switchProjectVersion(mod, selectedVersion)
try {
if (mod.has_update && mod.update_version_id === selectedVersion.id) {
await updateProject(mod)
} else {
await switchProjectVersion(mod, selectedVersion)
}
} finally {
resetUpdateState()
}
resetUpdateState()
}
}
@@ -1081,6 +1159,7 @@ provideContentManager({
deleteItem: removeMod,
bulkDeleteItems: (items: ContentItem[]) =>
Promise.all(items.map((item) => removeMod(item))).then(() => {}),
getDeleteDependencyWarning,
refresh: () => initProjects('must_revalidate'),
browse: handleBrowseContent,
uploadFiles: handleUploadFiles,
+18 -3
View File
@@ -406,6 +406,20 @@ function buildProjectHref(path, extraQuery = {}) {
return qs ? `${path}?${qs}` : path
}
function buildBrowseHref(path) {
const params = new URLSearchParams()
for (const [key, val] of Object.entries(route.query)) {
if (key === 'b') continue
if (Array.isArray(val)) {
for (const v of val) params.append(key, v)
} else if (val) {
params.append(key, String(val))
}
}
const qs = params.toString()
return qs ? `${path}?${qs}` : path
}
const projectDescriptionHref = computed(() => buildProjectHref(`/project/${route.params.id}`))
const versionsHref = computed(() =>
buildProjectHref(`/project/${route.params.id}/versions`, instanceFilters.value),
@@ -416,7 +430,7 @@ const projectBrowseBackUrl = computed(() => {
const browsePath = route.query.b
if (typeof browsePath === 'string' && browsePath.startsWith('/browse/')) return browsePath
const type = data.value?.project_type ? `${data.value.project_type}` : 'mod'
return `/browse/${type}`
return buildBrowseHref(`/browse/${type}`)
})
const projectInstallContext = computed(() => {
@@ -725,10 +739,11 @@ async function install(version) {
version,
instance.value ? instance.value.path : null,
'ProjectPage',
(version) => {
(version, installedProjectIds) => {
installing.value = false
if (instance.value && version) {
const installedIds = installedProjectIds ?? [data.value.id]
if (instance.value && version && installedIds.includes(data.value.id)) {
installed.value = true
installedVersion.value = version
}
@@ -5,7 +5,7 @@
:current-title="version.name"
:link-stack="[
{
href: `/project/${route.params.id}/versions`,
href: buildProjectHref(`/project/${route.params.id}/versions`),
label: 'Versions',
},
]"
@@ -249,6 +249,19 @@ const author = computed(() =>
const displayDependencies = ref({})
function buildProjectHref(path) {
const params = new URLSearchParams()
for (const [key, val] of Object.entries(route.query)) {
if (Array.isArray(val)) {
for (const v of val) params.append(key, v)
} else if (val) {
params.append(key, String(val))
}
}
const qs = params.toString()
return qs ? `${path}?${qs}` : path
}
async function refreshDisplayDependencies() {
const projectIds = new Set()
const versionIds = new Set()
@@ -282,7 +295,7 @@ async function refreshDisplayDependencies() {
icon: project?.icon_url,
title: project?.title || project?.name,
subtitle: `Version ${version.version_number} is ${dependency.dependency_type}`,
link: `/project/${project.slug}/version/${version.id}`,
link: buildProjectHref(`/project/${project.slug}/version/${version.id}`),
}
} else {
const project = dependencies.projects.find((obj) => obj.id === dependency.project_id)
@@ -292,7 +305,7 @@ async function refreshDisplayDependencies() {
icon: project?.icon_url,
title: project?.title || project?.name,
subtitle: `${dependency.dependency_type}`,
link: `/project/${project.slug}`,
link: buildProjectHref(`/project/${project.slug}`),
}
} else {
return {
@@ -5,7 +5,7 @@
:game-versions="gameVersions"
:versions="versions"
:project="project"
:version-link="(version) => `/project/${project.id}/version/${version.id}`"
:version-link="(version) => buildProjectHref(`/project/${project.id}/version/${version.id}`)"
>
<template #actions="{ version }">
<ButtonStyled circular type="transparent">
@@ -73,6 +73,7 @@ import {
ProjectPageVersions,
} from '@modrinth/ui'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { SwapIcon } from '@/assets/icons/index.js'
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
@@ -109,6 +110,20 @@ defineProps({
})
const { handleError } = injectNotificationManager()
const route = useRoute()
function buildProjectHref(path) {
const params = new URLSearchParams()
for (const [key, val] of Object.entries(route.query)) {
if (Array.isArray(val)) {
for (const v of val) params.append(key, v)
} else if (val) {
params.append(key, String(val))
}
}
const qs = params.toString()
return qs ? `${path}?${qs}` : path
}
const [loaders, gameVersions] = await Promise.all([
get_loaders().catch(handleError).then(ref),
@@ -42,6 +42,8 @@ interface ModpackAlreadyInstalledModalRef {
show: (instanceName: string, instancePath: string) => void
}
export type ContentInstallCallback = (versionId?: string, installedProjectIds?: string[]) => 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'])
@@ -102,7 +104,7 @@ export interface ContentInstallContext {
versionId?: string | null,
instancePath?: string | null,
source?: string,
callback?: (versionId?: string) => void,
callback?: ContentInstallCallback,
createInstanceCallback?: (profile: string) => void,
hints?: { preferredLoader?: string; preferredGameVersion?: string; showProjectInfo?: boolean },
) => Promise<void>
@@ -256,25 +258,25 @@ export function createContentInstall(opts: {
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 currentCallback: ContentInstallCallback = () => {}
let profileMap: Record<string, GameInstance> = {}
let incompatibilityWarningInstance: GameInstance | null = null
let incompatibilityWarningProject: Labrinth.Projects.v2.Project | null = null
let incompatibilityWarningCallback: (versionId?: string) => void = () => {}
let incompatibilityWarningCallback: ContentInstallCallback = () => {}
let incompatibilityWarningInstalled = false
let pendingModpackInstall: {
project: Labrinth.Projects.v2.Project
version: string
source: string
callback: (versionId?: string) => void
callback: ContentInstallCallback
createInstanceCallback: (profile: string) => void
} | null = null
async function showModInstallModal(
project: Labrinth.Projects.v2.Project,
versions: Labrinth.Versions.v2.Version[],
onInstall: (versionId?: string) => void,
onInstall: ContentInstallCallback,
hints?: { preferredLoader?: string; preferredGameVersion?: string; showProjectInfo?: boolean },
) {
currentProject = project
@@ -440,7 +442,7 @@ export function createContentInstall(opts: {
if (versionId && storeInstance) {
storeInstance.installed = true
}
currentCallback(versionId)
currentCallback(versionId, versionId && currentProject ? [currentProject.id] : undefined)
}
await showIncompatibilityWarning(
profile,
@@ -487,7 +489,7 @@ export function createContentInstall(opts: {
title: currentProject!.title,
source: 'ProjectInstallModal',
})
currentCallback(version.id)
currentCallback(version.id, installedProjectIds)
} catch (err) {
if (storeInstance) storeInstance.installing = false
opts.handleError(err)
@@ -501,7 +503,7 @@ export function createContentInstall(opts: {
project: Labrinth.Projects.v2.Project,
versions: Labrinth.Versions.v2.Version[],
version: Labrinth.Versions.v2.Version,
callback: (versionId?: string) => void,
callback: ContentInstallCallback,
) {
incompatibilityWarningInstance = instance
incompatibilityWarningProject = project
@@ -542,7 +544,7 @@ export function createContentInstall(opts: {
incompatibilityWarningInstalling.value = false
incompatibilityWarningInstalled = true
incompatibilityWarningCallback(version.id)
incompatibilityWarningCallback(version.id, [incompatibilityWarningProject.id])
incompatibilityWarningModalRef?.hide()
trackEvent('ProjectInstall', {
@@ -630,7 +632,7 @@ export function createContentInstall(opts: {
versionId?: string | null,
instancePath?: string | null,
source: string = 'unknown',
callback: (versionId?: string) => void = () => {},
callback: ContentInstallCallback = () => {},
createInstanceCallback: (profile: string) => void = () => {},
hints?: { preferredLoader?: string; preferredGameVersion?: string; showProjectInfo?: boolean },
) {
@@ -714,7 +716,7 @@ export function createContentInstall(opts: {
title: project.title,
source,
})
callback(version.id)
callback(version.id, installedProjectIds)
} finally {
removeInstallingItems(instancePath, installedProjectIds)
}
+14
View File
@@ -432,6 +432,20 @@ components:
primary_file:
type: string
description: The multipart field name of the primary file
environment:
type: string
enum:
- client_and_server
- client_only
- client_only_server_optional
- singleplayer_only
- server_only
- server_only_client_optional
- dedicated_server_only
- client_or_server
- client_or_server_prefers_both
- unknown
description: The environment that this version is for.
required:
- file_parts
- project_id
+2
View File
@@ -14,6 +14,8 @@ import { I18nDebugPanel, LoadingBar, NotificationPanel } from '@modrinth/ui'
import { setupProviders } from '~/providers/setup.ts'
import { useAuth } from './composables/auth'
const auth = await useAuth()
setupProviders(auth)
</script>
@@ -756,16 +756,16 @@ svg.inline-svg {
padding: var(--gap-16);
h2 {
@apply font-semibold;
font-size: var(--text-18);
font-weight: var(--weight-extrabold);
color: var(--color-contrast);
line-height: initial;
margin: 0;
}
h3 {
@apply font-semibold;
font-size: var(--text-16);
font-weight: var(--weight-bold);
color: var(--color-base);
margin: 0;
}
@@ -812,6 +812,7 @@ svg.inline-svg {
}
.details-list__item {
@apply font-normal;
@extend .iconified-list-item;
.details-list__item__text--style-secondary {
@@ -25,7 +25,7 @@
class="inline-flex items-center"
>
<button
v-tooltip="legendEntry.projectName ?? ''"
v-tooltip="legendEntry.tooltip ?? legendEntry.projectName ?? ''"
type="button"
class="inline-flex items-center gap-1.5 px-2 py-0.5 text-sm !outline-0 transition-all focus-within:!outline-0 focus:!outline-0 focus-visible:!outline-0"
:class="[
@@ -64,6 +64,7 @@ export function useAnalyticsChartLegend({
id: dataset.projectId,
name: dataset.label,
projectName: dataset.projectName,
tooltip: dataset.tooltip,
color: dataset.borderColor,
totalValue: getChartDatasetTotal(dataset),
hidden: hiddenDatasetIds.value.has(dataset.projectId),
@@ -118,6 +119,7 @@ export function useAnalyticsChartLegend({
id: getPreviousPeriodDatasetId(entry.id),
name: formatMessage(analyticsChartMessages.previousPeriodSuffix, { name: entry.name }),
projectName: entry.projectName,
tooltip: entry.tooltip,
color: entry.color,
totalValue: previousDataset ? getChartDatasetTotal(previousDataset) : 0,
hidden: hiddenDatasetIds.value.has(getPreviousPeriodDatasetId(entry.id)),
@@ -153,6 +155,7 @@ export function useAnalyticsChartLegend({
name: dataset.label,
}),
projectName: dataset.projectName,
tooltip: dataset.tooltip,
data: previousData,
borderColor: dataset.borderColor,
backgroundColor: dataset.backgroundColor,
@@ -296,7 +299,7 @@ export function useAnalyticsChartLegend({
}
function getLegendEntryTooltip(legendEntry: AnalyticsChartLegendEntry) {
return legendEntry.projectName ?? ''
return legendEntry.tooltip ?? legendEntry.projectName ?? ''
}
function isUnmonetizedLegendEntry(legendEntry: AnalyticsChartLegendEntry) {
@@ -97,7 +97,7 @@
"
/>
<span
v-tooltip="entry.projectName ?? ''"
v-tooltip="entry.tooltip ?? entry.projectName ?? ''"
class="min-w-0 truncate"
:class="{
'line-through': entry.hidden,
@@ -145,6 +145,7 @@ export type AnalyticsChartTooltipEntry = {
projectId: string
name: string
projectName?: string
tooltip?: string
color: string
formattedValue: string
hidden: boolean
@@ -208,6 +208,7 @@ const hoverEntries = computed(() => {
projectId: legendEntry.id,
name: legendEntry.name,
projectName: legendEntry.projectName,
tooltip: legendEntry.tooltip,
color: legendEntry.color,
formattedValue: props.isRatioMode
? `${ratioValue.toFixed(1)}%`
@@ -14,6 +14,7 @@ export type AnalyticsChartLegendEntry = {
id: string
name: string
projectName?: string
tooltip?: string
color: string
totalValue: number
hidden: boolean
@@ -1,20 +1,24 @@
import type { Labrinth } from '@modrinth/api-client'
import type {
AnalyticsBreakdownPreset,
AnalyticsDashboardProject,
AnalyticsDashboardStat,
AnalyticsGroupByPreset,
import {
type AnalyticsBreakdownPreset,
type AnalyticsDashboardProject,
type AnalyticsDashboardStat,
type AnalyticsGroupByPreset,
type AnalyticsSelectedFilters,
doesAnalyticsPointMatchNormalizedFilters,
normalizeAnalyticsSelectedFilters,
} from '~/providers/analytics/analytics'
import type { FormatMessage } from '../analytics-messages'
import {
analyticsChartMessages,
analyticsMessages,
analyticsStatCardMessages,
formatAnalyticsDependentProjectFallbackLabel,
formatAnalyticsDownloadReasonLabel,
formatAnalyticsLoaderLabel,
formatAnalyticsMonetizationLabel,
type FormatMessage,
} from '../analytics-messages'
import {
ALL_BREAKDOWN_VALUE,
@@ -22,6 +26,8 @@ import {
getAnalyticsBreakdownDatasetId,
getAnalyticsBreakdownKey,
getAnalyticsBreakdownValues,
isNoDependentAnalyticsBreakdownValue,
isUnknownAnalyticsBreakdownValue,
UNKNOWN_BREAKDOWN_VALUE,
} from '../breakdown'
import { PREVIOUS_PERIOD_DATASET_ID_PREFIX } from './analytics-chart-constants'
@@ -30,6 +36,7 @@ export type ChartDataset = {
projectId: string
label: string
projectName?: string
tooltip?: string
data: number[]
borderColor: string
backgroundColor: string
@@ -136,6 +143,7 @@ export function formatBreakdownLabel(
breakdownValue: string,
selectedBreakdown: AnalyticsBreakdownPreset,
getVersionDisplayName: ((versionId: string) => string) | undefined,
userNamesById: ReadonlyMap<string, string> | undefined,
formatMessage: FormatMessage,
): string {
const normalizedValue = breakdownValue.trim()
@@ -160,6 +168,15 @@ export function formatBreakdownLabel(
if (selectedBreakdown === 'download_reason') {
return formatAnalyticsDownloadReasonLabel(normalizedLowercaseValue, formatMessage)
}
if (selectedBreakdown === 'dependent_project_download') {
if (isNoDependentAnalyticsBreakdownValue(breakdownValue)) {
return formatMessage(analyticsMessages.noDependent)
}
return breakdownValue
}
if (selectedBreakdown === 'user_id') {
return userNamesById?.get(breakdownValue) ?? breakdownValue
}
if (selectedBreakdown === 'version_id') {
return getVersionDisplayName?.(breakdownValue) ?? breakdownValue
}
@@ -174,19 +191,34 @@ export function formatBreakdownLabels(
breakdownValues: readonly string[],
selectedBreakdowns: readonly AnalyticsBreakdownPreset[],
getVersionDisplayName: ((versionId: string) => string) | undefined,
userNamesById: ReadonlyMap<string, string> | undefined,
formatMessage: FormatMessage,
): string {
const normalizedBreakdowns = selectedBreakdowns.filter((breakdown) => breakdown !== 'none')
const downloadReasonBreakdownIndex = normalizedBreakdowns.indexOf('download_reason')
return collapseRepeatedUnknownBreakdownLabels(
selectedBreakdowns
.filter((breakdown) => breakdown !== 'none')
.map((breakdown, index) =>
formatBreakdownLabel(
breakdownValues[index] ?? '',
breakdown,
getVersionDisplayName,
normalizedBreakdowns.map((breakdown, index) => {
const breakdownValue = breakdownValues[index] ?? ''
if (
breakdown === 'dependent_project_download' &&
downloadReasonBreakdownIndex !== -1 &&
isUnknownAnalyticsBreakdownValue(breakdownValue)
) {
return formatAnalyticsDependentProjectFallbackLabel(
breakdownValues[downloadReasonBreakdownIndex],
formatMessage,
),
),
)
}
return formatBreakdownLabel(
breakdownValue,
breakdown,
getVersionDisplayName,
userNamesById,
formatMessage,
)
}),
formatMessage,
).join(COMBINED_BREAKDOWN_LABEL_SEPARATOR)
}
@@ -258,6 +290,33 @@ type PaletteRankEntry = {
total: number
}
function formatDatasetTooltip(projectName: string | undefined): string | undefined {
return projectName
}
function formatDependentProjectDatasetTooltip(
versionName: string | undefined,
dependentProjectName: string | undefined,
dependencyProjectNames: readonly string[],
formatMessage: FormatMessage,
): string | undefined {
if (dependencyProjectNames.length === 0) {
return undefined
}
if (versionName && dependentProjectName) {
return formatMessage(analyticsChartMessages.dependentProjectVersionTooltip, {
dependentProject: dependentProjectName,
dependencyProject: dependencyProjectNames.join(', '),
version: versionName,
})
}
return formatMessage(analyticsChartMessages.dependentOnProjectTooltip, {
project: dependencyProjectNames.join(', '),
})
}
function getPaletteColorForIndex(index: number, palette: string[]): string {
if (palette.length === 0) return ''
@@ -320,6 +379,10 @@ export function buildChartDatasets(
activeStat: AnalyticsDashboardStat,
palette: string[],
selectedBreakdowns: readonly AnalyticsBreakdownPreset[],
selectedFilters: AnalyticsSelectedFilters,
dependentProjectTypesById: ReadonlyMap<string, readonly string[]>,
projectNamesById: ReadonlyMap<string, string>,
userNamesById: ReadonlyMap<string, string>,
getVersionDisplayName: ((versionId: string) => string) | undefined,
getVersionProjectName: ((versionId: string) => string | undefined) | undefined,
formatMessage: FormatMessage,
@@ -332,17 +395,43 @@ export function buildChartDatasets(
const dataLength = Math.max(sliceCount, timeSlices.length)
const normalizedBreakdowns = selectedBreakdowns.filter((breakdown) => breakdown !== 'none')
const projectNamesById = new Map(selectedProjects.map((project) => [project.id, project.name]))
const normalizedFilters = normalizeAnalyticsSelectedFilters(selectedFilters)
function formatChartBreakdownLabels(breakdownValues: readonly string[]): string {
const downloadReasonBreakdownIndex = normalizedBreakdowns.indexOf('download_reason')
return collapseRepeatedUnknownBreakdownLabels(
normalizedBreakdowns.map((breakdown, index) => {
const breakdownValue = breakdownValues[index] ?? ''
if (breakdown === 'project') {
if (breakdown === 'project' || breakdown === 'dependent_project_download') {
if (
breakdown === 'dependent_project_download' &&
isNoDependentAnalyticsBreakdownValue(breakdownValue)
) {
return formatMessage(analyticsMessages.noDependent)
}
if (
breakdown === 'dependent_project_download' &&
isUnknownAnalyticsBreakdownValue(breakdownValue)
) {
return downloadReasonBreakdownIndex === -1
? formatMessage(analyticsMessages.unknown)
: formatAnalyticsDependentProjectFallbackLabel(
breakdownValues[downloadReasonBreakdownIndex],
formatMessage,
)
}
return projectNamesById.get(breakdownValue) ?? breakdownValue
}
return formatBreakdownLabel(breakdownValue, breakdown, getVersionDisplayName, formatMessage)
return formatBreakdownLabel(
breakdownValue,
breakdown,
getVersionDisplayName,
userNamesById,
formatMessage,
)
}),
formatMessage,
).join(COMBINED_BREAKDOWN_LABEL_SEPARATOR)
@@ -352,13 +441,27 @@ export function buildChartDatasets(
normalizedBreakdowns.length > 0 &&
!(normalizedBreakdowns.length === 1 && normalizedBreakdowns[0] === 'project')
) {
const hasVersionBreakdown = normalizedBreakdowns.includes('version_id')
const hasDependentProjectBreakdown = normalizedBreakdowns.includes('dependent_project_download')
const shouldShowDependentProjectTooltip =
hasDependentProjectBreakdown && (selectedProjects.length > 1 || hasVersionBreakdown)
const dataByBreakdown = new Map<string, number[]>()
const breakdownValuesByKey = new Map<string, string[]>()
const downloadTotalsByBreakdown = new Map<string, number>()
const dependentOnProjectIdsByBreakdown = new Map<string, Set<string>>()
timeSlices.forEach((slice, sliceIndex) => {
for (const point of slice) {
if (!isProjectAnalyticsPointInSelectedProjects(point, selectedProjectIds)) continue
if (
!doesAnalyticsPointMatchNormalizedFilters(
point,
normalizedFilters,
dependentProjectTypesById,
)
) {
continue
}
const breakdownValues = getAnalyticsBreakdownValues(
point,
@@ -374,6 +477,11 @@ export function buildChartDatasets(
dataByBreakdown.set(breakdownKey, new Array(dataLength).fill(0))
breakdownValuesByKey.set(breakdownKey, breakdownValues)
}
if (shouldShowDependentProjectTooltip && point.metric_kind === 'downloads') {
const projectIds = dependentOnProjectIdsByBreakdown.get(breakdownKey) ?? new Set<string>()
projectIds.add(point.source_project)
dependentOnProjectIdsByBreakdown.set(breakdownKey, projectIds)
}
if (point.metric_kind === 'downloads') {
downloadTotalsByBreakdown.set(
@@ -402,6 +510,42 @@ export function buildChartDatasets(
return Array.from(dataByBreakdown.entries()).map(([breakdownKey, data]) => {
const breakdownValues = breakdownValuesByKey.get(breakdownKey) ?? []
const fallbackColor = colorsByBreakdown.get(breakdownKey) ?? ''
const versionBreakdownIndex = normalizedBreakdowns.indexOf('version_id')
const dependentProjectBreakdownIndex = normalizedBreakdowns.indexOf(
'dependent_project_download',
)
const versionName =
hasVersionBreakdown && versionBreakdownIndex !== -1
? getVersionDisplayName?.(breakdownValues[versionBreakdownIndex] ?? '')
: undefined
const dependentProjectId =
dependentProjectBreakdownIndex !== -1
? breakdownValues[dependentProjectBreakdownIndex]
: undefined
const dependentProjectName = dependentProjectId
? isMissingDependentProjectValue(dependentProjectId)
? undefined
: (projectNamesById.get(dependentProjectId) ?? dependentProjectId)
: undefined
const versionProjectName =
normalizedBreakdowns.length === 1 && normalizedBreakdowns[0] === 'version_id'
? getVersionProjectName?.(breakdownValues[0] ?? '')
: undefined
const dependencyProjectNames = [...(dependentOnProjectIdsByBreakdown.get(breakdownKey) ?? [])]
.map((projectId) => projectNamesById.get(projectId) ?? projectId)
.sort((left, right) => left.localeCompare(right))
const dependentProjectTooltip = dependentProjectId
? isNoDependentAnalyticsBreakdownValue(dependentProjectId)
? formatMessage(analyticsMessages.noDependentTooltip)
: isUnknownAnalyticsBreakdownValue(dependentProjectId)
? formatMessage(analyticsMessages.unknown)
: formatDependentProjectDatasetTooltip(
versionName,
dependentProjectName,
dependencyProjectNames,
formatMessage,
)
: undefined
const color =
normalizedBreakdowns.length === 1
? getBreakdownColor(
@@ -414,10 +558,8 @@ export function buildChartDatasets(
return {
projectId: getAnalyticsBreakdownDatasetId(breakdownValues, normalizedBreakdowns),
label: formatChartBreakdownLabels(breakdownValues),
projectName:
normalizedBreakdowns.length === 1 && normalizedBreakdowns[0] === 'version_id'
? getVersionProjectName?.(breakdownValues[0] ?? '')
: undefined,
projectName: versionProjectName,
tooltip: dependentProjectTooltip ?? formatDatasetTooltip(versionProjectName),
data,
borderColor: color,
backgroundColor: color,
@@ -432,6 +574,15 @@ export function buildChartDatasets(
timeSlices.forEach((slice, sliceIndex) => {
for (const point of slice) {
if (!isProjectAnalyticsPointInSelectedProjects(point, selectedProjectIds)) continue
if (
!doesAnalyticsPointMatchNormalizedFilters(
point,
normalizedFilters,
dependentProjectTypesById,
)
) {
continue
}
if (point.metric_kind === 'downloads') {
downloadTotal += getMetricValue(point, 'downloads')
@@ -481,6 +632,15 @@ export function buildChartDatasets(
timeSlices.forEach((slice, sliceIndex) => {
for (const point of slice) {
if (!isProjectAnalyticsPointInSelectedProjects(point, selectedProjectIds)) continue
if (
!doesAnalyticsPointMatchNormalizedFilters(
point,
normalizedFilters,
dependentProjectTypesById,
)
) {
continue
}
const breakdownValues = getAnalyticsBreakdownValues(
point,
@@ -526,6 +686,10 @@ export function buildChartDatasets(
return Array.from(dataByProjectBreakdown.entries()).map(([breakdownKey, data]) => {
const breakdownValues = breakdownValuesByKey.get(breakdownKey) ?? []
const fallbackColor = colorsByBreakdown.get(breakdownKey) ?? ''
const versionProjectName =
normalizedBreakdowns.length === 1 && normalizedBreakdowns[0] === 'version_id'
? getVersionProjectName?.(breakdownValues[0] ?? '')
: undefined
const color =
normalizedBreakdowns.length === 1
? getBreakdownColor(
@@ -538,10 +702,8 @@ export function buildChartDatasets(
return {
projectId: getAnalyticsBreakdownDatasetId(breakdownValues, normalizedBreakdowns),
label: formatChartBreakdownLabels(breakdownValues),
projectName:
normalizedBreakdowns.length === 1 && normalizedBreakdowns[0] === 'version_id'
? getVersionProjectName?.(breakdownValues[0] ?? '')
: undefined,
projectName: versionProjectName,
tooltip: formatDatasetTooltip(versionProjectName),
data,
borderColor: color,
backgroundColor: color,
@@ -549,6 +711,10 @@ export function buildChartDatasets(
})
}
function isMissingDependentProjectValue(value: string | undefined): boolean {
return isUnknownAnalyticsBreakdownValue(value) || isNoDependentAnalyticsBreakdownValue(value)
}
export function getSliceCount(
timeRange: Labrinth.Analytics.v3.TimeRange,
fallback: number,
@@ -1,12 +1,15 @@
import type { Labrinth } from '@modrinth/api-client'
import { useVIntl } from '@modrinth/ui'
import { computed, type ComputedRef, ref, watch } from 'vue'
import { useTheme } from '~/composables/nuxt-accessors'
import { isDarkTheme } from '~/plugins/theme/index.ts'
import type {
AnalyticsBreakdownPreset,
AnalyticsDashboardContextValue,
AnalyticsDashboardProject,
AnalyticsDashboardStat,
AnalyticsSelectedFilters,
} from '~/providers/analytics/analytics'
import {
@@ -47,12 +50,16 @@ export function useAnalyticsChartDatasets(
| 'displayedPreviousTimeSlices'
| 'displayedSelectedGroupBy'
| 'displayedSelectedBreakdowns'
| 'displayedSelectedFilters'
| 'hiddenGraphDatasetIds'
| 'hasExplicitGraphDatasetSelection'
| 'isGraphDatasetSelectionActive'
| 'selectedGraphDatasetIds'
| 'defaultGraphDatasetIds'
| 'topGraphDatasetIds'
| 'projectNamesById'
| 'userNamesById'
| 'dependentProjectTypesById'
| 'getVersionDisplayName'
| 'getVersionProjectName'
>,
@@ -160,6 +167,10 @@ export function useAnalyticsChartDatasets(
selectedProjects.value,
legendPalette.value,
context.displayedSelectedBreakdowns.value,
context.displayedSelectedFilters.value,
context.dependentProjectTypesById.value,
context.projectNamesById.value,
context.userNamesById.value,
context.getVersionDisplayName,
showProjectVersionNames.value ? context.getVersionProjectName : undefined,
formatMessage,
@@ -172,6 +183,10 @@ export function useAnalyticsChartDatasets(
selectedProjects.value,
legendPalette.value,
context.displayedSelectedBreakdowns.value,
context.displayedSelectedFilters.value,
context.dependentProjectTypesById.value,
context.projectNamesById.value,
context.userNamesById.value,
context.getVersionDisplayName,
showProjectVersionNames.value ? context.getVersionProjectName : undefined,
formatMessage,
@@ -357,12 +372,16 @@ export function useAnalyticsChartDatasets(
}
function buildDatasetsByStat(
timeSlices: Parameters<typeof buildChartDatasets>[0],
timeSlices: Labrinth.Analytics.v3.TimeSlice[],
selectedProjects: AnalyticsDashboardProject[],
palette: string[],
selectedBreakdowns: Parameters<typeof buildChartDatasets>[4],
getVersionDisplayName: Parameters<typeof buildChartDatasets>[5],
getVersionProjectName: Parameters<typeof buildChartDatasets>[6],
selectedBreakdowns: readonly AnalyticsBreakdownPreset[],
selectedFilters: AnalyticsSelectedFilters,
dependentProjectTypesById: ReadonlyMap<string, readonly string[]>,
projectNamesById: ReadonlyMap<string, string>,
userNamesById: ReadonlyMap<string, string>,
getVersionDisplayName: (versionId: string) => string,
getVersionProjectName: ((versionId: string) => string | undefined) | undefined,
formatMessage: FormatMessage,
sliceCount: number,
) {
@@ -374,6 +393,10 @@ function buildDatasetsByStat(
stat,
palette,
selectedBreakdowns,
selectedFilters,
dependentProjectTypesById,
projectNamesById,
userNamesById,
getVersionDisplayName,
getVersionProjectName,
formatMessage,
@@ -13,6 +13,7 @@ export type AnalyticsBreakdownItemType =
| 'downloadSource'
| 'gameVersion'
| 'loader'
| 'member'
| 'monetization'
| 'project'
| 'projectVersion'
@@ -83,6 +84,14 @@ export const analyticsMessages = defineMessages({
id: 'analytics.value.unknown',
defaultMessage: 'Unknown',
},
noDependent: {
id: 'analytics.value.no-dependent',
defaultMessage: 'None',
},
noDependentTooltip: {
id: 'analytics.value.no-dependent-tooltip',
defaultMessage: 'Downloaded for reasons other than being a dependency',
},
other: {
id: 'analytics.value.other',
defaultMessage: 'Other',
@@ -175,6 +184,14 @@ export const analyticsMessages = defineMessages({
id: 'analytics.filter.search.project-versions',
defaultMessage: 'Search project versions...',
},
searchDependentProjectsPlaceholder: {
id: 'analytics.filter.search.dependent-projects',
defaultMessage: 'Search projects...',
},
searchMembersPlaceholder: {
id: 'analytics.filter.search.members',
defaultMessage: 'Search members...',
},
searchVersionsPlaceholder: {
id: 'analytics.filter.search.versions',
defaultMessage: 'Search versions...',
@@ -351,6 +368,22 @@ export const analyticsBreakdownMessages = defineMessages({
id: 'analytics.breakdown.download-reason',
defaultMessage: 'Download reason',
},
members: {
id: 'analytics.breakdown.members',
defaultMessage: 'Member',
},
dependentProjectDownload: {
id: 'analytics.breakdown.dependent-project-download',
defaultMessage: 'Dependent project',
},
dependentProjectType: {
id: 'analytics.breakdown.dependent-project-type',
defaultMessage: 'Dependent project type',
},
dependentOn: {
id: 'analytics.breakdown.dependent-on',
defaultMessage: 'Dependent on',
},
versionId: {
id: 'analytics.breakdown.project-version',
defaultMessage: 'Project version',
@@ -517,25 +550,33 @@ export const analyticsChartMessages = defineMessages({
id: 'analytics.chart.action.show-top-eight',
defaultMessage: 'Show top 8',
},
dependentOnProjectTooltip: {
id: 'analytics.chart.tooltip.dependent-on-project',
defaultMessage: 'Dependent on {project}',
},
dependentProjectVersionTooltip: {
id: 'analytics.chart.tooltip.dependent-project-version',
defaultMessage: '{dependentProject} dependent on {dependencyProject}, {version}',
},
tableSelectionLimited: {
id: 'analytics.chart.table-selection.limited',
defaultMessage:
'Showing {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}}}} from table',
'Showing {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}}} member {{limit, plural, one {member} other {members}}} 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}}}} from table',
},
tableSelectionAll: {
id: 'analytics.chart.table-selection.all',
defaultMessage:
'Showing all {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}}}} from table',
'Showing all {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}}} member {{count, plural, one {member} other {members}}} 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}}}} from table',
},
tableSelectionTop: {
id: 'analytics.chart.table-selection.top',
defaultMessage:
'Showing 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}}}} from table',
'Showing 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}}} member {{count, plural, one {member} other {members}}} 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}}}} from table',
},
tableSelectionCount: {
id: 'analytics.chart.table-selection.count',
defaultMessage:
'Showing {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}}}} from table',
'Showing {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}}} member {{count, plural, one {member} other {members}}} 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}}}} from table',
},
lineView: {
id: 'analytics.chart.view.line',
@@ -781,6 +822,10 @@ export function formatAnalyticsBreakdownLabel(
return formatMessage(analyticsBreakdownMessages.userAgent)
case 'download_reason':
return formatMessage(analyticsBreakdownMessages.downloadReason)
case 'user_id':
return formatMessage(analyticsBreakdownMessages.members)
case 'dependent_project_download':
return formatMessage(analyticsBreakdownMessages.dependentProjectDownload)
case 'version_id':
return formatMessage(analyticsBreakdownMessages.versionId)
case 'loader':
@@ -810,6 +855,10 @@ export function getAnalyticsBreakdownItemType(
return 'downloadSource'
case 'download_reason':
return 'downloadReason'
case 'user_id':
return 'member'
case 'dependent_project_download':
return 'project'
case 'version_id':
return 'projectVersion'
case 'loader':
@@ -853,6 +902,18 @@ export function formatAnalyticsDownloadReasonLabel(
}
}
export function formatAnalyticsDependentProjectFallbackLabel(
downloadReason: string | undefined,
formatMessage: FormatMessage,
): string {
const normalizedReason = downloadReason?.trim().toLowerCase()
if (normalizedReason === 'standalone' || normalizedReason === 'update') {
return formatMessage(analyticsMessages.noDependent)
}
return formatMessage(analyticsMessages.unknown)
}
export function formatAnalyticsDownloadSourceLabel(
source: string,
formatMessage: FormatMessage,
@@ -19,6 +19,8 @@ import type {
MutableRouteQuery,
} from '~/providers/analytics/analytics-types'
import { getAnalyticsBreakdownsWithSharedStats } from './query-builder/query-filter-utils'
export const DEFAULT_TIMEFRAME_PRESET: AnalyticsTimeframePreset = 'last_30_days'
export const DEFAULT_TIMEFRAME_MODE: AnalyticsTimeframeMode = 'preset'
export const DEFAULT_LAST_TIMEFRAME_AMOUNT = 1
@@ -73,9 +75,11 @@ const BREAKDOWN_PRESET_VALUES: AnalyticsBreakdownPreset[] = [
'monetization',
'user_agent',
'download_reason',
'user_id',
'version_id',
'loader',
'game_version',
'dependent_project_download',
]
const ANALYTICS_DASHBOARD_STAT_VALUES: AnalyticsDashboardStat[] = [
@@ -89,15 +93,18 @@ const ANALYTICS_GRAPH_VIEW_MODE_VALUES: AnalyticsGraphViewMode[] = ['line', 'are
const ANALYTICS_TABLE_SORT_COLUMN_VALUES: AnalyticsTableSortColumn[] = [
'date',
'project',
'dependent_on',
'breakdown',
'breakdown_project',
'breakdown_country',
'breakdown_monetization',
'breakdown_user_agent',
'breakdown_download_reason',
'breakdown_user_id',
'breakdown_version_id',
'breakdown_loader',
'breakdown_game_version',
'breakdown_dependent_project_download',
'views',
'downloads',
'revenue',
@@ -131,9 +138,12 @@ const QUERY_KEY_FILTER_MONETIZATION = 'a_monetization'
const QUERY_KEY_FILTER_USER_AGENT = 'a_user_agent'
const QUERY_KEY_FILTER_LEGACY_DOWNLOAD_SOURCE = 'a_download_source'
const QUERY_KEY_FILTER_DOWNLOAD_REASON = 'a_download_reason'
const QUERY_KEY_FILTER_USER_ID = 'a_user_id'
const QUERY_KEY_FILTER_VERSION_ID = 'a_version_id'
const QUERY_KEY_FILTER_GAME_VERSION = 'a_game_version'
const QUERY_KEY_FILTER_LOADER_TYPE = 'a_loader_type'
const QUERY_KEY_FILTER_DEPENDENT_PROJECT_ID = 'a_dependent_project_id'
const QUERY_KEY_FILTER_DEPENDENT_PROJECT_TYPE = 'a_dependent_project_type'
const QUERY_KEY_STAT = 'a_stat'
const QUERY_KEY_GRAPH_VIEW_MODE = 'a_chart'
const QUERY_KEY_GRAPH_RATIO_MODE = 'a_ratio'
@@ -154,9 +164,12 @@ const URL_FILTER_CATEGORIES: Exclude<AnalyticsQueryFilterCategory, 'project'>[]
'monetization',
'user_agent',
'download_reason',
'user_id',
'version_id',
'game_version',
'loader_type',
'dependent_project_id',
'dependent_project_type',
]
const FILTER_QUERY_KEY_BY_CATEGORY: Record<
@@ -168,9 +181,12 @@ const FILTER_QUERY_KEY_BY_CATEGORY: Record<
monetization: QUERY_KEY_FILTER_MONETIZATION,
user_agent: QUERY_KEY_FILTER_USER_AGENT,
download_reason: QUERY_KEY_FILTER_DOWNLOAD_REASON,
user_id: QUERY_KEY_FILTER_USER_ID,
version_id: QUERY_KEY_FILTER_VERSION_ID,
game_version: QUERY_KEY_FILTER_GAME_VERSION,
loader_type: QUERY_KEY_FILTER_LOADER_TYPE,
dependent_project_id: QUERY_KEY_FILTER_DEPENDENT_PROJECT_ID,
dependent_project_type: QUERY_KEY_FILTER_DEPENDENT_PROJECT_TYPE,
}
const ANALYTICS_QUERY_KEYS = [
@@ -189,9 +205,12 @@ const ANALYTICS_QUERY_KEYS = [
QUERY_KEY_FILTER_USER_AGENT,
QUERY_KEY_FILTER_LEGACY_DOWNLOAD_SOURCE,
QUERY_KEY_FILTER_DOWNLOAD_REASON,
QUERY_KEY_FILTER_USER_ID,
QUERY_KEY_FILTER_VERSION_ID,
QUERY_KEY_FILTER_GAME_VERSION,
QUERY_KEY_FILTER_LOADER_TYPE,
QUERY_KEY_FILTER_DEPENDENT_PROJECT_ID,
QUERY_KEY_FILTER_DEPENDENT_PROJECT_TYPE,
QUERY_KEY_STAT,
QUERY_KEY_GRAPH_VIEW_MODE,
QUERY_KEY_GRAPH_RATIO_MODE,
@@ -212,9 +231,12 @@ export function buildEmptySelectedFilters(): AnalyticsSelectedFilters {
monetization: [],
user_agent: [],
download_reason: [],
user_id: [],
version_id: [],
game_version: [],
loader_type: [],
dependent_project_id: [],
dependent_project_type: [],
}
}
@@ -255,7 +277,7 @@ function normalizeFilterQueryValues(
.filter((value) => PROJECT_STATUS_FILTER_VALUES.includes(value))
}
if (category !== 'loader_type') {
if (category !== 'loader_type' && category !== 'dependent_project_type') {
return values
}
@@ -456,7 +478,7 @@ export function getAnalyticsBreakdownPresetsForProjectSelection(
}
}
return normalizedBreakdowns
return getAnalyticsBreakdownsWithSharedStats(normalizedBreakdowns)
}
export function getAnalyticsBreakdownPresetForProjectSelection(
@@ -776,6 +798,12 @@ export function hasAnalyticsProjectSelectionQuery(query: LocationQuery): boolean
return parseListQueryValue(query[QUERY_KEY_PROJECT_IDS]).length > 0
}
export function hasAnalyticsAllProjectSelectionQuery(query: LocationQuery): boolean {
return parseListQueryValue(query[QUERY_KEY_PROJECT_IDS]).includes(
PROJECT_SELECTION_ALL_QUERY_VALUE,
)
}
export function hasAnalyticsGraphProjectEventsVisibilityQuery(query: LocationQuery): boolean {
return query[QUERY_KEY_GRAPH_PROJECT_EVENTS_VISIBILITY] !== undefined
}
@@ -0,0 +1,62 @@
<template>
<div class="mr-2.5 flex min-w-0 items-center gap-2">
<span
v-if="!hideIcon"
v-tooltip="iconTooltip"
class="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded text-primary"
>
<img
v-if="iconUrl"
:src="iconUrl"
:alt="formatMessage(analyticsMessages.projectIconAlt, { name: label })"
class="h-6 w-6 rounded object-cover"
/>
<BoxIcon v-else class="h-full w-full" />
</span>
<component
:is="labelHref ? 'a' : 'span'"
v-tooltip="labelTooltip"
:href="labelHref"
:target="labelHref ? '_blank' : undefined"
:rel="labelHref ? 'noopener noreferrer' : undefined"
class="line-clamp-2 min-w-0 truncate text-wrap font-semibold leading-tight text-primary"
:class="{ 'hover:underline': labelHref }"
:title="label"
>
{{ label }}
</component>
<component
:is="organizationHref ? 'a' : 'span'"
v-if="organizationTooltip"
v-tooltip="organizationTooltip"
:href="organizationHref"
:target="organizationHref ? '_blank' : undefined"
:rel="organizationHref ? 'noopener noreferrer' : undefined"
:aria-label="organizationTooltip"
class="flex size-4 shrink-0 items-center text-primary"
:class="{ 'hover:underline': organizationHref }"
>
<OrganizationIcon class="size-4" />
</component>
</div>
</template>
<script setup lang="ts">
import { BoxIcon, OrganizationIcon } from '@modrinth/assets'
import { useVIntl } from '@modrinth/ui'
import { analyticsMessages } from '../analytics-messages.ts'
defineProps<{
label: string
iconUrl?: string
iconTooltip?: string
hideIcon?: boolean
labelHref?: string
labelTooltip?: string
organizationHref?: string
organizationTooltip?: string
}>()
const { formatMessage } = useVIntl()
</script>
@@ -23,7 +23,6 @@ type BuildAnalyticsTableColumnsOptions = {
selectedBreakdowns: readonly AnalyticsTableBreakdownPreset[]
selectedFilters: AnalyticsSelectedFilters
showBreakdownColumn: boolean
showProjectVersionProjectColumn: boolean
formatMessage: FormatMessage
getRelevantAnalyticsDashboardStats: (
breakdowns: readonly AnalyticsBreakdownPreset[],
@@ -43,7 +42,6 @@ export function buildAnalyticsTableColumns({
selectedBreakdowns,
selectedFilters,
showBreakdownColumn,
showProjectVersionProjectColumn,
formatMessage,
getRelevantAnalyticsDashboardStats,
}: BuildAnalyticsTableColumnsOptions): TableColumn<AnalyticsTableColumnKey>[] {
@@ -66,20 +64,11 @@ export function buildAnalyticsTableColumns({
key: getAnalyticsTableBreakdownColumnKey(breakdown),
label: getAnalyticsTableBreakdownColumnLabel(breakdown, formatMessage),
enableSorting: true,
width: breakdown === 'project' ? '25%' : undefined,
width: breakdown === 'project' && selectedBreakdowns.length === 1 ? '45%' : undefined,
})
}
}
if (showProjectVersionProjectColumn) {
nextColumns.push({
key: 'project',
label: formatAnalyticsBreakdownLabel('project', formatMessage),
enableSorting: true,
width: '25%',
})
}
for (const stat of stats) {
const column = getAnalyticsTableMetricColumn(stat, formatMessage)
if (column) {
@@ -102,6 +91,7 @@ export function getAnalyticsTableMetricColumn(
enableSorting: true,
defaultSortDirection: 'desc',
align: 'right',
width: '20%',
}
case 'downloads':
return {
@@ -110,6 +100,7 @@ export function getAnalyticsTableMetricColumn(
enableSorting: true,
defaultSortDirection: 'desc',
align: 'right',
width: '20%',
}
case 'revenue':
return {
@@ -118,6 +109,7 @@ export function getAnalyticsTableMetricColumn(
enableSorting: true,
defaultSortDirection: 'desc',
align: 'right',
width: '20%',
}
case 'playtime':
return {
@@ -126,6 +118,7 @@ export function getAnalyticsTableMetricColumn(
enableSorting: true,
defaultSortDirection: 'desc',
align: 'right',
width: '20%',
}
default:
return null
@@ -67,6 +67,8 @@ function getAnalyticsTableCsvCellValue(
return row.date
case 'project':
return row.project
case 'dependent_on':
return row.dependent_on
case 'breakdown':
return row.breakdownDisplay
case 'views':
@@ -1,8 +1,11 @@
import type { Labrinth } from '@modrinth/api-client'
import type {
AnalyticsBreakdownPreset,
AnalyticsDashboardStat,
import {
type AnalyticsBreakdownPreset,
type AnalyticsDashboardStat,
type AnalyticsSelectedFilters,
doesAnalyticsPointMatchNormalizedFilters,
normalizeAnalyticsSelectedFilters,
} from '~/providers/analytics/analytics'
import {
@@ -12,13 +15,18 @@ import {
getSliceCount,
} from '../analytics-chart/analytics-chart-utils'
import type { FormatMessage } from '../analytics-messages'
import { analyticsMessages } from '../analytics-messages'
import {
analyticsMessages,
formatAnalyticsDependentProjectFallbackLabel,
} from '../analytics-messages'
import {
ALL_BREAKDOWN_VALUE,
COMBINED_BREAKDOWN_LABEL_SEPARATOR,
getAnalyticsBreakdownDatasetId,
getAnalyticsBreakdownKey,
getAnalyticsBreakdownValues,
isNoDependentAnalyticsBreakdownValue,
isUnknownAnalyticsBreakdownValue,
} from '../breakdown'
import { getAnalyticsTableBreakdownColumnKey } from './analytics-table-columns'
import type {
@@ -36,8 +44,12 @@ type BuildAnalyticsTableRowsOptions = {
timeSlices: Labrinth.Analytics.v3.TimeSlice[]
selectedBreakdowns: readonly AnalyticsTableBreakdownPreset[]
selectedProjectIds: ReadonlySet<string>
selectedFilters: AnalyticsSelectedFilters
dependentProjectTypesById: ReadonlyMap<string, readonly string[]>
includeDependentProjectTooltipContext: boolean
relevantStats: ReadonlySet<AnalyticsDashboardStat>
projectNamesById: ReadonlyMap<string, string>
userNamesById: ReadonlyMap<string, string>
getVersionDisplayName: (versionId: string) => string
getVersionProjectName: (versionId: string) => string | undefined
showTimeInBucketLabel: boolean
@@ -51,8 +63,12 @@ export function buildAnalyticsTableRows({
timeSlices,
selectedBreakdowns,
selectedProjectIds,
selectedFilters,
dependentProjectTypesById,
includeDependentProjectTooltipContext,
relevantStats,
projectNamesById,
userNamesById,
getVersionDisplayName,
getVersionProjectName,
showTimeInBucketLabel,
@@ -72,6 +88,7 @@ export function buildAnalyticsTableRows({
const projectDisplayValues = new Map<string, string>()
const nextRows = new Map<string, AnalyticsTableRow>()
const bucketLabelsBySliceIndex = new Map<number, { date: string; dateMs: number }>()
const normalizedFilters = normalizeAnalyticsSelectedFilters(selectedFilters)
function getBreakdownDisplayValue(
breakdownValue: string,
@@ -84,6 +101,7 @@ export function buildAnalyticsTableRows({
breakdownValue,
breakdown,
projectNamesById,
userNamesById,
getVersionDisplayName,
formatMessage,
)
@@ -111,6 +129,15 @@ export function buildAnalyticsTableRows({
return displayValue
}
function getProjectVersionIdForBreakdownValues(breakdownValues: readonly string[]) {
const versionBreakdownIndex = selectedBreakdowns.indexOf('version_id')
if (versionBreakdownIndex === -1) {
return ''
}
return breakdownValues[versionBreakdownIndex] ?? ''
}
function getBreakdownDisplays(breakdownValues: readonly string[]) {
const displays: AnalyticsTableBreakdownDisplayValues = {}
@@ -118,6 +145,19 @@ export function buildAnalyticsTableRows({
displays[breakdown] = getBreakdownDisplayValue(breakdownValues[index] ?? '', breakdown)
})
const dependentProjectBreakdownIndex = selectedBreakdowns.indexOf('dependent_project_download')
const downloadReasonBreakdownIndex = selectedBreakdowns.indexOf('download_reason')
if (
dependentProjectBreakdownIndex !== -1 &&
downloadReasonBreakdownIndex !== -1 &&
isUnknownAnalyticsBreakdownValue(breakdownValues[dependentProjectBreakdownIndex])
) {
displays.dependent_project_download = formatAnalyticsDependentProjectFallbackLabel(
breakdownValues[downloadReasonBreakdownIndex],
formatMessage,
)
}
return displays
}
@@ -157,6 +197,7 @@ export function buildAnalyticsTableRows({
function createRow(
rowId: string,
breakdownValues: readonly string[],
dependentOnProjectId?: string,
bucketLabel?: { date: string; dateMs: number },
) {
const breakdownKey =
@@ -169,6 +210,12 @@ export function buildAnalyticsTableRows({
date: bucketLabel?.date ?? '',
dateMs: bucketLabel?.dateMs ?? 0,
project: getProjectDisplayValueForBreakdownValues(breakdownValues),
projectVersionId: getProjectVersionIdForBreakdownValues(breakdownValues),
dependent_on: dependentOnProjectId
? (projectNamesById.get(dependentOnProjectId) ?? dependentOnProjectId)
: '',
dependentOnProjectId: dependentOnProjectId ?? '',
dependentOnProjectIds: dependentOnProjectId ? [dependentOnProjectId] : [],
breakdown: breakdownKey,
breakdownValues: Object.fromEntries(
selectedBreakdowns.map((breakdown, index) => [breakdown, breakdownValues[index] ?? '']),
@@ -190,6 +237,18 @@ export function buildAnalyticsTableRows({
return row
}
function addDependentOnProjectIdToRow(row: AnalyticsTableRow, projectId: string | undefined) {
if (!projectId || row.dependentOnProjectIds.includes(projectId)) {
return
}
row.dependentOnProjectIds.push(projectId)
if (!row.dependentOnProjectId) {
row.dependentOnProjectId = projectId
row.dependent_on = projectNamesById.get(projectId) ?? projectId
}
}
if (!includeDate && selectedBreakdowns.length === 0) {
createRow(ALL_PROJECTS_BREAKDOWN_VALUE, [])
}
@@ -211,6 +270,15 @@ export function buildAnalyticsTableRows({
if (!selectedProjectIds.has(point.source_project)) {
continue
}
if (
!doesAnalyticsPointMatchNormalizedFilters(
point,
normalizedFilters,
dependentProjectTypesById,
)
) {
continue
}
const pointStat = getAnalyticsTableStatForMetric(point.metric_kind)
if (!pointStat || !relevantStats.has(pointStat)) {
@@ -226,12 +294,21 @@ export function buildAnalyticsTableRows({
}
const nextBucketLabel = includeDate ? (bucketLabel ?? getBucketLabel(sliceIndex)) : undefined
const dependentOnProjectId = includeDependentProjectTooltipContext
? point.source_project
: undefined
const dependentTooltipProjectId = selectedBreakdowns.includes('dependent_project_download')
? point.source_project
: undefined
const breakdownKey =
breakdownValues.length === 0
? ALL_PROJECTS_BREAKDOWN_VALUE
: getAnalyticsBreakdownKey(breakdownValues)
const rowId = includeDate ? `${nextBucketLabel?.dateMs ?? 0}::${breakdownKey}` : breakdownKey
const row = nextRows.get(rowId) ?? createRow(rowId, breakdownValues, nextBucketLabel)
const row =
nextRows.get(rowId) ??
createRow(rowId, breakdownValues, dependentOnProjectId, nextBucketLabel)
addDependentOnProjectIdToRow(row, dependentTooltipProjectId)
addAnalyticsMetricToTableRow(row, point)
}
})
@@ -292,11 +369,21 @@ function formatAnalyticsTableBreakdownDisplayValue(
value: string,
breakdown: AnalyticsTableBreakdownPreset,
projectNamesById: ReadonlyMap<string, string>,
userNamesById: ReadonlyMap<string, string>,
getVersionDisplayName: (versionId: string) => string,
formatMessage: FormatMessage,
): string {
if (breakdown === 'project') {
if (breakdown === 'project' || breakdown === 'dependent_project_download') {
if (breakdown === 'dependent_project_download') {
if (isNoDependentAnalyticsBreakdownValue(value)) {
return formatMessage(analyticsMessages.noDependent)
}
if (isUnknownAnalyticsBreakdownValue(value)) {
return formatMessage(analyticsMessages.unknown)
}
}
return projectNamesById.get(value) ?? value
}
return formatBreakdownLabel(value, breakdown, getVersionDisplayName, formatMessage)
return formatBreakdownLabel(value, breakdown, getVersionDisplayName, userNamesById, formatMessage)
}
@@ -3,7 +3,7 @@ import type { TableColumn } from '@modrinth/ui'
import { isAnalyticsTableBreakdownColumnKey } from './analytics-table-columns'
import type { AnalyticsTableColumnKey, AnalyticsTableRow } from './analytics-table-types'
const SEARCHABLE_COLUMN_KEYS = new Set<AnalyticsTableColumnKey>(['date', 'project'])
const SEARCHABLE_COLUMN_KEYS = new Set<AnalyticsTableColumnKey>(['date', 'project', 'dependent_on'])
export function getAnalyticsTableSearchableColumns(
columns: TableColumn<AnalyticsTableColumnKey>[],
@@ -39,6 +39,8 @@ function getAnalyticsTableSearchableCellValue(
return row.date
case 'project':
return row.project
case 'dependent_on':
return row.dependent_on
case 'breakdown':
return row.breakdownDisplay
default:
@@ -129,6 +129,15 @@ function getAnalyticsTableRowComparator(
directionFactor,
sortCollator,
)
case 'dependent_on':
return (left, right) =>
compareAnalyticsTableRows(
left,
right,
sortCollator.compare(left.dependent_on, right.dependent_on),
directionFactor,
sortCollator,
)
case 'breakdown':
return (left, right) =>
compareAnalyticsTableRows(
@@ -18,11 +18,15 @@ export type AnalyticsTableSortState = {
export type AnalyticsTableSortDirectionValue = AnalyticsTableSortDirection
export type AnalyticsTableRow = {
[key: string]: string | number | AnalyticsTableBreakdownDisplayValues
[key: string]: string | number | string[] | AnalyticsTableBreakdownDisplayValues
id: string
date: string
dateMs: number
project: string
projectVersionId: string
dependent_on: string
dependentOnProjectId: string
dependentOnProjectIds: string[]
breakdown: string
breakdownValues: AnalyticsTableBreakdownDisplayValues
breakdownDisplays: AnalyticsTableBreakdownDisplayValues
@@ -55,32 +55,92 @@
<template #cell-date="{ value }">
<span class="text-primary">{{ value }}</span>
</template>
<template #cell-breakdown_project="{ value }">
<span class="text-primary">{{ value }}</span>
<template #cell-breakdown_project="{ row, value }">
<ProjectCell
:label="getProjectCellLabel(value)"
:icon-url="getProjectIconUrl(row.breakdownValues.project)"
:icon-tooltip="getProjectCellLabel(value)"
:label-href="getProjectPageHref(row.breakdownValues.project)"
:organization-href="getProjectOrganizationPageHref(row.breakdownValues.project)"
:organization-tooltip="getProjectOrganizationName(row.breakdownValues.project)"
/>
</template>
<template #cell-breakdown_country="{ value }">
<span class="text-primary">{{ value }}</span>
<span class="mr-2.5 text-primary">{{ value }}</span>
</template>
<template #cell-breakdown_monetization="{ value }">
<span class="text-primary">{{ value }}</span>
<span class="mr-2.5 text-primary">{{ value }}</span>
</template>
<template #cell-breakdown_user_agent="{ value }">
<span class="text-primary">{{ value }}</span>
<span class="mr-2.5 text-primary">{{ value }}</span>
</template>
<template #cell-breakdown_download_reason="{ value }">
<span class="text-primary">{{ value }}</span>
<span class="mr-2.5 text-primary">{{ value }}</span>
</template>
<template #cell-breakdown_version_id="{ value }">
<span class="text-primary">{{ value }}</span>
<template #cell-breakdown_user_id="{ row, value }">
<component
:is="getUserPageHref(row.breakdownValues.user_id) ? 'a' : 'span'"
:href="getUserPageHref(row.breakdownValues.user_id)"
:target="getUserPageHref(row.breakdownValues.user_id) ? '_blank' : undefined"
:rel="getUserPageHref(row.breakdownValues.user_id) ? 'noopener noreferrer' : undefined"
class="mr-2.5 flex min-w-0 items-center gap-2 text-primary"
:class="{ 'hover:underline': getUserPageHref(row.breakdownValues.user_id) }"
>
<span
v-tooltip="getUserCellLabel(value)"
class="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-full text-primary"
>
<img
v-if="getUserAvatarUrl(row.breakdownValues.user_id)"
:src="getUserAvatarUrl(row.breakdownValues.user_id)"
:alt="getUserCellLabel(value)"
class="h-6 w-6 rounded-full object-cover"
/>
<UserIcon v-else class="h-full w-full" />
</span>
<span class="min-w-0 truncate font-semibold leading-tight text-primary">
{{ value }}
</span>
</component>
</template>
<template #cell-breakdown_dependent_project_download="{ row, value }">
<ProjectCell
:label="getProjectCellLabel(value)"
:icon-url="
isMissingDependentProjectValue(row.breakdownValues.dependent_project_download)
? undefined
: getProjectIconUrl(row.breakdownValues.dependent_project_download)
"
:icon-tooltip="getProjectCellLabel(value)"
:hide-icon="
isMissingDependentProjectValue(row.breakdownValues.dependent_project_download)
"
:label-href="
isMissingDependentProjectValue(row.breakdownValues.dependent_project_download)
? undefined
: getProjectPageHref(row.breakdownValues.dependent_project_download)
"
:label-tooltip="getDependentProjectTooltip(row)"
/>
</template>
<template #cell-breakdown_version_id="{ row, value }">
<component
:is="getVersionPageHref(row.projectVersionId) ? 'a' : 'span'"
v-tooltip="getVersionProjectName(row.projectVersionId)"
:href="getVersionPageHref(row.projectVersionId)"
:target="getVersionPageHref(row.projectVersionId) ? '_blank' : undefined"
:rel="getVersionPageHref(row.projectVersionId) ? 'noopener noreferrer' : undefined"
class="mr-2.5 text-primary"
:class="{ 'hover:underline': getVersionPageHref(row.projectVersionId) }"
>
{{ value }}
</component>
</template>
<template #cell-breakdown_loader="{ value }">
<span class="text-primary">{{ value }}</span>
<span class="mr-2.5 text-primary">{{ value }}</span>
</template>
<template #cell-breakdown_game_version="{ value }">
<span class="text-primary">{{ value }}</span>
</template>
<template #cell-project="{ value }">
<span class="text-primary">{{ value }}</span>
<span class="mr-2.5 text-primary">{{ value }}</span>
</template>
<template #cell-views="{ row }">
<span>{{ formatInteger(row.views) }}</span>
@@ -130,7 +190,7 @@
</template>
<script setup lang="ts">
import { DownloadIcon, DropdownIcon, SearchIcon } from '@modrinth/assets'
import { DownloadIcon, DropdownIcon, SearchIcon, UserIcon } from '@modrinth/assets'
import {
ButtonStyled,
OverflowMenu,
@@ -159,10 +219,15 @@ import {
} from '../analytics-chart/analytics-chart-utils.ts'
import {
analyticsBreakdownMessages,
analyticsChartMessages,
analyticsMessages,
analyticsTableMessages,
} from '../analytics-messages.ts'
import AnalyticsLoadingBar from '../AnalyticsLoadingBar.vue'
import {
isNoDependentAnalyticsBreakdownValue,
isUnknownAnalyticsBreakdownValue,
} from '../breakdown.ts'
import {
buildAnalyticsTableColumns,
getAnalyticsTableBreakdownColumnLabel,
@@ -194,8 +259,10 @@ import { sortAnalyticsTableRows } from './analytics-table-sorting.ts'
import type {
AnalyticsTableColumnKey,
AnalyticsTableMode,
AnalyticsTableRow,
AnalyticsTableSortDirectionValue,
} from './analytics-table-types.ts'
import ProjectCell from './ProjectCell.vue'
import { useAnalyticsTableGraphSelection } from './use-analytics-table-graph-selection.ts'
import { useAnalyticsTablePagination } from './use-analytics-table-pagination.ts'
import { useAnalyticsTableRowCache } from './use-analytics-table-row-cache.ts'
@@ -221,7 +288,15 @@ const {
getRelevantAnalyticsDashboardStats,
isLoading,
versionNumbersById,
versionProjectIdsById,
versionProjectNamesById,
projectNamesById,
projectIconUrlsById,
projectOrganizationIdsById,
projectOrganizationNamesById,
userNamesById,
userAvatarUrlsById,
dependentProjectTypesById,
getVersionDisplayName,
getVersionProjectName,
} = injectAnalyticsDashboardContext()
@@ -263,11 +338,10 @@ const showGraphDatasetSelection = computed(() =>
? selectedProjectIdSet.value.size > 1
: selectedBreakdowns.value.length > 0,
)
const showProjectVersionProjectColumn = computed(
const includeDependentProjectTooltipContext = computed(
() =>
selectedBreakdownSet.value.has('version_id') &&
!selectedBreakdownSet.value.has('project') &&
selectedProjectIdSet.value.size > 1,
selectedBreakdownSet.value.has('dependent_project_download') &&
!selectedBreakdownSet.value.has('project'),
)
const includeDateColumn = computed(
() =>
@@ -314,9 +388,6 @@ const csvExportOptions = computed<OverflowMenuOption[]>(() => {
},
]
})
const projectNamesById = computed(
() => new Map(projects.value.map((project) => [project.id, project.name])),
)
const hasAvailableProjects = computed(() => projects.value.length > 0)
const analyticsPointCount = computed(() =>
timeSlices.value.reduce((sum, slice) => sum + slice.length, 0),
@@ -360,8 +431,12 @@ function buildTableRows(mode: AnalyticsTableMode) {
timeSlices: timeSlices.value,
selectedBreakdowns: selectedBreakdowns.value,
selectedProjectIds: selectedProjectIdSet.value,
selectedFilters: selectedFilters.value,
dependentProjectTypesById: dependentProjectTypesById.value,
includeDependentProjectTooltipContext: includeDependentProjectTooltipContext.value,
relevantStats: relevantStats.value,
projectNamesById: projectNamesById.value,
userNamesById: userNamesById.value,
getVersionDisplayName,
getVersionProjectName,
showTimeInBucketLabel: showTimeInBucketLabel.value,
@@ -379,12 +454,86 @@ function buildColumns(includeDate: boolean) {
selectedBreakdowns: selectedBreakdowns.value,
selectedFilters: selectedFilters.value,
showBreakdownColumn: showBreakdownColumn.value,
showProjectVersionProjectColumn: showProjectVersionProjectColumn.value,
formatMessage,
getRelevantAnalyticsDashboardStats,
})
}
function getProjectIconUrl(projectId: string | undefined) {
return projectId ? projectIconUrlsById.value.get(projectId) : undefined
}
function getProjectOrganizationName(projectId: string | undefined) {
return projectId ? projectOrganizationNamesById.value.get(projectId) : undefined
}
function getProjectCellLabel(value: unknown) {
return typeof value === 'string' ? value : String(value ?? '')
}
function getUserAvatarUrl(userId: string | undefined) {
return userId ? userAvatarUrlsById.value.get(userId) : undefined
}
function getUserCellLabel(value: unknown) {
return typeof value === 'string' ? value : String(value ?? '')
}
function getUserPageHref(userId: string | undefined) {
if (!userId) return undefined
const username = userNamesById.value.get(userId) ?? userId
return `/user/${encodeURIComponent(username)}`
}
function getProjectPageHref(projectId: string | undefined) {
return projectId ? `/project/${encodeURIComponent(projectId)}` : undefined
}
function getProjectOrganizationPageHref(projectId: string | undefined) {
if (!projectId) return undefined
const organizationId = projectOrganizationIdsById.value.get(projectId)
if (!organizationId) return undefined
return `/organization/${encodeURIComponent(organizationId)}`
}
function getVersionPageHref(versionId: string | undefined) {
if (!versionId) return undefined
const projectId = versionProjectIdsById.value.get(versionId)
if (!projectId) return undefined
return `/project/${encodeURIComponent(projectId)}/version/${encodeURIComponent(versionId)}`
}
function isMissingDependentProjectValue(value: string | undefined) {
return isUnknownAnalyticsBreakdownValue(value) || isNoDependentAnalyticsBreakdownValue(value)
}
function getDependentProjectTooltip(row: AnalyticsTableRow) {
if (isNoDependentAnalyticsBreakdownValue(row.breakdownValues.dependent_project_download)) {
return formatMessage(analyticsMessages.noDependentTooltip)
}
if (isUnknownAnalyticsBreakdownValue(row.breakdownValues.dependent_project_download)) {
return formatMessage(analyticsMessages.unknown)
}
const dependencyProjectIds = new Set(row.dependentOnProjectIds)
if (row.dependentOnProjectId) {
dependencyProjectIds.add(row.dependentOnProjectId)
}
const dependencyProjectNames = [...dependencyProjectIds]
.map((projectId) => projectNamesById.value.get(projectId) ?? projectId)
.sort((left, right) => left.localeCompare(right))
return dependencyProjectNames.length > 0
? formatMessage(analyticsChartMessages.dependentOnProjectTooltip, {
project: dependencyProjectNames.join(', '),
})
: undefined
}
watch(
activeColumns,
(nextColumns) => {
@@ -445,6 +594,9 @@ watch(
selectedBreakdowns,
selectedFilters,
projects,
dependentProjectTypesById,
projectNamesById,
userNamesById,
versionNumbersById,
versionProjectNamesById,
],
@@ -7,6 +7,10 @@ import type {
AnalyticsSelectedBreakdowns,
} from '~/providers/analytics/analytics'
import {
isNoDependentAnalyticsBreakdownValue,
isUnknownAnalyticsBreakdownValue,
} from '../breakdown'
import { getAnalyticsTableMetricSortedGraphDatasetIds } from './analytics-table-sorting'
import type { AnalyticsTableColumnKey, AnalyticsTableRow } from './analytics-table-types'
@@ -58,6 +62,9 @@ export function useAnalyticsTableGraphSelection({
const filteredSelectableGraphDatasetIds = computed(() =>
getAnalyticsTableSelectableGraphDatasetIds(filteredRows.value),
)
const excludedGraphDatasetIds = computed(() =>
getAnalyticsTableExcludedGraphDatasetIds(sortedRows.value),
)
const sortedMetricGraphDatasetIds = computed(() =>
getAnalyticsTableMetricSortedGraphDatasetIds(sortedRows.value, sortColumn.value, sortCollator),
)
@@ -65,7 +72,9 @@ export function useAnalyticsTableGraphSelection({
const sortedMetricIds = sortedMetricGraphDatasetIds.value
const defaultIds =
sortedMetricIds.length > 0 ? sortedMetricIds : selectableGraphDatasetIds.value
return defaultIds.slice(0, graphDatasetSelectionLimit)
return defaultIds
.filter((id) => !excludedGraphDatasetIds.value.has(id))
.slice(0, graphDatasetSelectionLimit)
})
const tableSelectedGraphDatasetIds = computed<unknown[]>({
get: () => selectedGraphDatasetIds.value,
@@ -149,7 +158,9 @@ export function useAnalyticsTableGraphSelection({
defaultGraphDatasetIds.value = nextShowGraphDatasetSelection
? [...nextDefaultGraphDatasetIds]
: []
topGraphDatasetIds.value = nextShowGraphDatasetSelection ? [...nextTopGraphDatasetIds] : []
topGraphDatasetIds.value = nextShowGraphDatasetSelection
? nextTopGraphDatasetIds.filter((id) => !excludedGraphDatasetIds.value.has(id))
: []
},
{ immediate: true },
)
@@ -184,6 +195,20 @@ export function useAnalyticsTableGraphSelection({
return Array.from(new Set(rows.map((row) => row.graphDatasetId)))
}
function getAnalyticsTableExcludedGraphDatasetIds(rows: AnalyticsTableRow[]): Set<string> {
return new Set(
rows
.filter((row) =>
Object.values(row.breakdownValues).some(
(value) =>
isUnknownAnalyticsBreakdownValue(value) ||
isNoDependentAnalyticsBreakdownValue(value),
),
)
.map((row) => row.graphDatasetId),
)
}
return {
filteredSelectableGraphDatasetIds,
tableSelectedGraphDatasetIds,
@@ -6,6 +6,7 @@ import { formatAnalyticsDownloadSourceLabel, type FormatMessage } from './analyt
export const ALL_BREAKDOWN_VALUE = '__all__'
export const UNKNOWN_BREAKDOWN_VALUE = '__unknown__'
export const NO_DEPENDENT_BREAKDOWN_VALUE = '__no_dependent__'
export const COMBINED_BREAKDOWN_LABEL_SEPARATOR = ' + '
export const COMBINED_BREAKDOWN_DATASET_ID_PREFIX = 'breakdowns:'
@@ -41,6 +42,19 @@ export function getAnalyticsBreakdownValue(
'reason' in point ? point.reason : undefined,
UNKNOWN_BREAKDOWN_VALUE,
)
case 'user_id':
return normalizeBreakdownValue('user_id' in point ? point.user_id : undefined)
case 'dependent_project_download': {
const dependentProjectId = normalizeBreakdownValue(
'dependent_project_id' in point ? point.dependent_project_id : undefined,
UNKNOWN_BREAKDOWN_VALUE,
)
const downloadReason = 'reason' in point ? point.reason?.trim().toLowerCase() : undefined
return dependentProjectId === UNKNOWN_BREAKDOWN_VALUE &&
(downloadReason === 'standalone' || downloadReason === 'update')
? NO_DEPENDENT_BREAKDOWN_VALUE
: dependentProjectId
}
case 'version_id':
return normalizeBreakdownValue('version_id' in point ? point.version_id : undefined)
case 'loader':
@@ -94,16 +108,30 @@ export function getDownloadSourceLabel(value: string, formatMessage: FormatMessa
return formatAnalyticsDownloadSourceLabel(value, formatMessage)
}
export function isUnknownAnalyticsBreakdownValue(value: string | null | undefined): boolean {
const normalized = value?.trim()
if (!normalized) {
return false
}
const normalizedLowercase = normalized.toLowerCase()
return (
normalized === UNKNOWN_BREAKDOWN_VALUE ||
normalizedLowercase === 'unknown' ||
normalizedLowercase === 'other'
)
}
export function isNoDependentAnalyticsBreakdownValue(value: string | null | undefined): boolean {
return value?.trim() === NO_DEPENDENT_BREAKDOWN_VALUE
}
function normalizeBreakdownValue(
value: string | undefined,
fallback = ALL_BREAKDOWN_VALUE,
): string {
const normalized = value?.trim()
const normalizedLowercase = normalized?.toLowerCase()
if (
fallback === UNKNOWN_BREAKDOWN_VALUE &&
(normalizedLowercase === 'unknown' || normalizedLowercase === 'other')
) {
if (fallback === UNKNOWN_BREAKDOWN_VALUE && isUnknownAnalyticsBreakdownValue(normalized)) {
return fallback
}
return normalized && normalized.length > 0 ? normalized : fallback
@@ -25,10 +25,25 @@
<template #option="{ category, option, selected }">
<div class="flex min-w-0 flex-1 items-center gap-2">
<template v-if="category.key === 'version_id'">
<span
v-if="category.key === 'user_id'"
v-tooltip="option.label"
class="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-full text-primary"
:class="selected ? 'text-contrast' : 'text-primary'"
>
<img
v-if="getUserAvatarUrl(option.value)"
:src="getUserAvatarUrl(option.value)"
:alt="option.label"
class="h-6 w-6 rounded-full object-cover"
/>
<UserIcon v-else class="h-full w-full" />
</span>
<template
v-for="metadata in getFilterOptionProjectMetadata(category.key, option.value)"
:key="`${category.key}-${option.value}-${metadata.name}`"
>
<span
v-for="metadata in getProjectVersionOptionProjectMetadata(option.value)"
:key="`${option.value}-${metadata.name}`"
v-tooltip="metadata.name"
class="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded text-primary"
>
@@ -42,6 +57,8 @@
</span>
</template>
<span
:ref="(element) => setFilterOptionLabelRef(category.key, option.value, element)"
v-tooltip="getFilterOptionLabelTooltip(category.key, option.value, option.label)"
class="min-w-0 truncate font-semibold leading-tight"
:class="selected ? 'text-contrast' : 'text-primary'"
>
@@ -179,16 +196,24 @@
</template>
<script setup lang="ts">
import { BoxIcon } from '@modrinth/assets'
import { BoxIcon, UserIcon } from '@modrinth/assets'
import {
buildDependentsSearchFilters,
DropdownFilterBar,
type DropdownFilterBarCategory,
type DropdownFilterBarOption,
injectModrinthClient,
injectNotificationManager,
type ProjectType,
Tabs,
type TabsTab,
type TabsValue,
truncatedTooltip,
useVIntl,
} from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import type { ComponentPublicInstance } from 'vue'
import { useFormattedCountries } from '@/composables/country.ts'
import {
@@ -258,6 +283,8 @@ const props = withDefaults(
)
const { formatMessage } = useVIntl()
const client = injectModrinthClient()
const { addNotification } = injectNotificationManager()
const {
hasProjectContext,
projects,
@@ -277,6 +304,9 @@ const {
versionPublishedDatesById,
versionProjectNamesById,
versionProjectIconUrlsById,
projectNamesById,
userNamesById,
userAvatarUrlsById,
getVersionDisplayName,
} = injectAnalyticsDashboardContext()
const formattedCountries = useFormattedCountries()
@@ -290,6 +320,17 @@ const gameVersionTypeTabs = computed<TabsTab[]>(() => [
{ value: 'release', label: formatMessage(analyticsMessages.releaseTab) },
{ value: 'all', label: formatMessage(analyticsMessages.allTab) },
])
const dependentProjectSearchProjectTypes: readonly ProjectType[] = [
'mod',
'modpack',
'resourcepack',
'shader',
'datapack',
'plugin',
]
const dependentProjectInitialSearchLimit = 100
const dependentProjectQuerySearchLimit = 500
const dependentProjectSearchDebounceMs = 250
const resolvedAddLabel = computed(
() => props.addLabel ?? formatMessage(analyticsMessages.addButton),
)
@@ -306,14 +347,16 @@ const projectStatusFilterOptions = computed<DropdownFilterBarOption[]>(() =>
})),
)
const selectedProjectIdSet = computed(() => new Set(selectedProjectIds.value))
const effectiveSelectedProjectCount = computed(
() =>
projects.value.filter(
const effectiveSelectedProjectIds = computed(() =>
projects.value
.filter(
(project) =>
selectedProjectIdSet.value.has(project.id) &&
doesProjectStatusMatchFilters(project.status, selectedFilters.value),
).length,
)
.map((project) => project.id),
)
const effectiveSelectedProjectCount = computed(() => effectiveSelectedProjectIds.value.length)
const showProjectVersionProjectIcons = computed(() => effectiveSelectedProjectCount.value > 1)
const defaultSelectedBreakdown = computed(() =>
getDefaultAnalyticsBreakdownPresets(selectedProjectIds.value),
@@ -330,12 +373,72 @@ const projectVersionFilterOptions = shallowRef<ProjectVersionFilterOption[]>([])
const projectVersionFilterOptionProjectMetadataById = shallowRef(
new Map<string, ProjectVersionFilterOptionProjectMetadata[]>(),
)
const dependentProjectSearchInput = ref('')
const dependentProjectSearchQuery = ref('')
const draftSelectedFilters = ref<AnalyticsSelectedFilters>(
cloneSelectedFilters(selectedFilters.value),
)
let selectedFiltersCommitRequestId = 0
let projectVersionFilterOptionsCacheKey = ''
let projectVersionFilterOptionProjectMetadataCacheKey = ''
let dependentProjectSearchDebounceTimeout: ReturnType<typeof setTimeout> | null = null
const filterOptionLabelElements = new Map<string, HTMLElement>()
const filterOptionLabelRefUpdateToken = ref(0)
const dependentProjectSearchFilters = computed(() =>
buildDependentsSearchFilters(
dependentProjectSearchProjectTypes,
effectiveSelectedProjectIds.value,
),
)
const dependentProjectSearchQueryValue = computed(() => dependentProjectSearchQuery.value.trim())
const dependentProjectSearchResultLimit = computed(() =>
dependentProjectSearchQueryValue.value.length > 0
? dependentProjectQuerySearchLimit
: dependentProjectInitialSearchLimit,
)
const selectedDependentProjectIds = computed(() => [
...new Set([
...selectedFilters.value.dependent_project_id,
...draftSelectedFilters.value.dependent_project_id,
]),
])
const { data: dependentProjectSearchResults, error: dependentProjectSearchError } = useQuery({
queryKey: computed(() => [
'analytics',
'query-filter',
'dependent-project-search',
dependentProjectSearchFilters.value,
dependentProjectSearchQueryValue.value,
dependentProjectSearchResultLimit.value,
]),
queryFn: () =>
client.labrinth.projects_v2.search({
query: dependentProjectSearchQueryValue.value || undefined,
new_filters: dependentProjectSearchFilters.value,
limit: dependentProjectSearchResultLimit.value,
index: 'downloads',
}),
enabled: computed(
() =>
effectiveSelectedProjectIds.value.length > 0 &&
dependentProjectSearchFilters.value.length > 0,
),
placeholderData: (previousData) => previousData,
refetchOnWindowFocus: false,
})
const { data: selectedDependentProjects } = useQuery({
queryKey: computed(() => [
'analytics',
'query-filter',
'selected-dependent-projects',
selectedDependentProjectIds.value,
]),
queryFn: () => client.labrinth.projects_v2.getMultiple(selectedDependentProjectIds.value),
enabled: computed(() => selectedDependentProjectIds.value.length > 0),
placeholderData: [],
refetchOnWindowFocus: false,
})
const selectedFilterValue = computed<Record<string, string[]>>({
get: () => getSelectedFilterBarValue(),
@@ -374,6 +477,27 @@ watch(queryResetToken, () => {
clearDownloadsThresholds()
})
watch(dependentProjectSearchInput, (query) => {
if (dependentProjectSearchDebounceTimeout) {
clearTimeout(dependentProjectSearchDebounceTimeout)
}
dependentProjectSearchDebounceTimeout = setTimeout(() => {
dependentProjectSearchQuery.value = query.trim()
dependentProjectSearchDebounceTimeout = null
}, dependentProjectSearchDebounceMs)
})
watch(dependentProjectSearchError, (error) => {
if (!error) return
addNotification({
title: 'Dependent projects failed to load',
text: getDependentProjectSearchErrorMessage(error),
type: 'error',
})
})
watch(
selectedFilters,
(nextFilters, previousFilters) => {
@@ -475,6 +599,12 @@ watch(
{ immediate: true },
)
onBeforeUnmount(() => {
if (dependentProjectSearchDebounceTimeout) {
clearTimeout(dependentProjectSearchDebounceTimeout)
}
})
async function scheduleSelectedFiltersCommit() {
const requestId = ++selectedFiltersCommitRequestId
const nextFilters = cloneSelectedFilters(draftSelectedFilters.value)
@@ -577,6 +707,40 @@ const filterCategories = computed<DropdownFilterBarCategory[]>(() => {
label: formatMessage(analyticsBreakdownMessages.loader),
options: withSelectedOptions('loader_type', loaderTypeFilterOptions.value),
},
{
key: 'dependent_project_id',
label: formatMessage(analyticsBreakdownMessages.dependentProjectDownload),
searchable: true,
disableLocalOptionsFilter: true,
searchPlaceholder: formatMessage(analyticsMessages.searchDependentProjectsPlaceholder),
emptyOptionsLabel: analyticsFilterOptionsEmptyLabel.value,
emptySearchLabel: analyticsFilterOptionsEmptyLabel.value,
onSearchQueryChange: setDependentProjectSearchInput,
options: withSelectedOptions('dependent_project_id', dependentProjectFilterOptions.value),
submenuClass: 'w-fit min-w-[18rem]',
previewDropdownWidth: 'fit-content',
previewDropdownMinWidth: '18rem',
},
{
key: 'dependent_project_type',
label: formatMessage(analyticsBreakdownMessages.dependentProjectType),
options: withSelectedOptions(
'dependent_project_type',
dependentProjectTypeFilterOptions.value,
),
},
{
key: 'user_id',
label: formatMessage(analyticsBreakdownMessages.members),
searchable: memberFilterOptions.value.length > 6,
searchPlaceholder: formatMessage(analyticsMessages.searchMembersPlaceholder),
emptyOptionsLabel: analyticsFilterOptionsEmptyLabel.value,
emptySearchLabel: analyticsFilterOptionsEmptyLabel.value,
options: withSelectedOptions('user_id', memberFilterOptions.value),
submenuClass: 'w-fit min-w-[14rem]',
previewDropdownWidth: 'fit-content',
previewDropdownMinWidth: '14rem',
},
)
return categories.filter((category) =>
@@ -636,6 +800,16 @@ const downloadReasonFilterOptions = computed<DropdownFilterBarOption[]>(() =>
})),
)
const memberFilterOptions = computed<DropdownFilterBarOption[]>(() =>
filterOptions.value.userIds
.map((userId) => ({
value: userId,
label: getUserFilterOptionLabel(userId),
searchTerms: [userId],
}))
.sort((left, right) => left.label.localeCompare(right.label)),
)
const gameVersionFilterOptions = computed<DropdownFilterBarOption[]>(() =>
filterOptions.value.gameVersions
.filter((gameVersion) => {
@@ -668,6 +842,45 @@ const loaderTypeFilterOptions = computed<DropdownFilterBarOption[]>(() =>
.sort((left, right) => left.label.localeCompare(right.label)),
)
const dependentProjectSearchResultsById = computed(
() =>
new Map(
(dependentProjectSearchResults.value?.hits ?? []).map((project) => [
project.project_id,
project,
]),
),
)
const selectedDependentProjectsById = computed(
() => new Map((selectedDependentProjects.value ?? []).map((project) => [project.id, project])),
)
const dependentProjectFilterOptions = computed<DropdownFilterBarOption[]>(() => {
const optionsById = new Map<string, DropdownFilterBarOption>()
for (const project of dependentProjectSearchResults.value?.hits ?? []) {
optionsById.set(project.project_id, {
value: project.project_id,
label: project.title,
searchTerms: [project.project_id, project.slug, project.author].filter(
(term): term is string => Boolean(term),
),
})
}
return [...optionsById.values()]
})
const dependentProjectTypeFilterOptions = computed<DropdownFilterBarOption[]>(() =>
filterOptions.value.dependentProjectTypes
.map((projectType) => ({
value: projectType,
label: getProjectTypeFilterOptionLabel(projectType),
searchTerms: [projectType],
}))
.sort((left, right) => left.label.localeCompare(right.label)),
)
function isAnalyticsFilterValueCategory(
categoryKey: string,
): categoryKey is AnalyticsFilterValueCategory {
@@ -697,15 +910,77 @@ function getMissingSelectedOptionLabel(
if (categoryKey === 'download_reason') {
return getDownloadReasonFilterOptionLabel
}
if (categoryKey === 'user_id') {
return getUserFilterOptionLabel
}
if (categoryKey === 'user_agent') {
return (value) => getDownloadSourceLabel(value, formatMessage)
}
if (categoryKey === 'loader_type') {
return getLoaderTypeFilterOptionLabel
}
if (categoryKey === 'dependent_project_id') {
return getDependentProjectFilterOptionLabel
}
if (categoryKey === 'dependent_project_type') {
return getProjectTypeFilterOptionLabel
}
return undefined
}
function getUserAvatarUrl(userId: string): string | undefined {
return userAvatarUrlsById.value.get(userId)
}
function setFilterOptionLabelRef(
categoryKey: string,
optionValue: string,
element: Element | ComponentPublicInstance | null,
) {
const key = getFilterOptionLabelElementKey(categoryKey, optionValue)
if (element instanceof HTMLElement) {
if (filterOptionLabelElements.get(key) === element) {
return
}
filterOptionLabelElements.set(key, element)
filterOptionLabelRefUpdateToken.value++
return
}
if (filterOptionLabelElements.delete(key)) {
filterOptionLabelRefUpdateToken.value++
}
}
function getFilterOptionLabelTooltip(
categoryKey: string,
optionValue: string,
label: string,
): string | undefined {
void filterOptionLabelRefUpdateToken.value
return truncatedTooltip(
filterOptionLabelElements.get(getFilterOptionLabelElementKey(categoryKey, optionValue)),
label,
)
}
function getFilterOptionLabelElementKey(categoryKey: string, optionValue: string) {
return `${categoryKey}\x1f${optionValue}`
}
function getFilterOptionProjectMetadata(categoryKey: string, optionValue: string) {
if (categoryKey === 'version_id') {
return getProjectVersionOptionProjectMetadata(optionValue)
}
if (categoryKey === 'dependent_project_id') {
return getDependentProjectOptionProjectMetadata(optionValue)
}
return []
}
function getProjectVersionOptionProjectMetadata(versionId: string) {
if (!showProjectVersionProjectIcons.value) {
return []
@@ -714,6 +989,24 @@ function getProjectVersionOptionProjectMetadata(versionId: string) {
return projectVersionFilterOptionProjectMetadataById.value.get(versionId) ?? []
}
function getDependentProjectOptionProjectMetadata(projectId: string) {
const searchResult = dependentProjectSearchResultsById.value.get(projectId)
const selectedProject = selectedDependentProjectsById.value.get(projectId)
const name =
searchResult?.title ??
selectedProject?.title ??
projectNamesById.value.get(projectId) ??
projectId
const iconUrl = searchResult?.icon_url ?? selectedProject?.icon_url
return [
{
name,
...(iconUrl ? { iconUrl } : {}),
},
]
}
function getCountryFilterOptionLabel(countryCode: string): string {
const normalizedCode = countryCode.trim().toUpperCase()
if (normalizedCode === 'XX') {
@@ -731,10 +1024,51 @@ function getLoaderTypeFilterOptionLabel(loaderType: string): string {
return formatAnalyticsLoaderLabel(loaderType, formatMessage)
}
function getProjectTypeFilterOptionLabel(projectType: string): string {
return formatProjectType(projectType)
}
function getDependentProjectFilterOptionLabel(projectId: string): string {
return (
dependentProjectSearchResultsById.value.get(projectId)?.title ??
selectedDependentProjectsById.value.get(projectId)?.title ??
projectNamesById.value.get(projectId) ??
projectId
)
}
function getDownloadReasonFilterOptionLabel(reason: string): string {
return formatAnalyticsDownloadReasonLabel(reason, formatMessage)
}
function getUserFilterOptionLabel(userId: string): string {
return userNamesById.value.get(userId) ?? userId
}
function setDependentProjectSearchInput(query: string) {
dependentProjectSearchInput.value = query
}
function getDependentProjectSearchErrorMessage(error: unknown): string {
if (error && typeof error === 'object') {
const dataDescription = (error as { data?: { description?: unknown } }).data?.description
if (typeof dataDescription === 'string' && dataDescription.length > 0) {
return dataDescription
}
const message = (error as { message?: unknown }).message
if (typeof message === 'string' && message.length > 0) {
return message
}
}
if (typeof error === 'string' && error.length > 0) {
return error
}
return 'Please try searching again or changing the selected projects.'
}
function getDateTimestamp(date: string | undefined): number | undefined {
if (!date) {
return undefined
@@ -12,6 +12,7 @@
:placeholder="formatMessage(analyticsMessages.selectProjects)"
:no-options-message="noProjectsMessage"
:searchable="projectOptions.length > 6"
fuzzy-search
:max-tag-rows="1"
:trigger-class="analyticsQueryChipTriggerClass"
fit-content
@@ -79,7 +80,7 @@
@keydown.enter.stop
@keydown.space.stop
>
<LayersIcon
<UserIcon
class="h-5 w-5 shrink-0 text-primary"
:class="isUserProjectsOptionSelected ? 'text-contrast' : 'text-primary'"
/>
@@ -211,6 +212,7 @@
:placeholder="formatMessage(analyticsMessages.selectProjects)"
:no-options-message="noProjectsMessage"
:searchable="projectOptions.length > 6"
fuzzy-search
:max-tag-rows="1"
checkbox-position="right"
show-selection-actions
@@ -290,7 +292,7 @@
@keydown.enter.stop
@keydown.space.stop
>
<LayersIcon
<UserIcon
class="h-5 w-5 shrink-0 text-primary"
:class="isUserProjectsOptionSelected ? 'text-contrast' : 'text-primary'"
/>
@@ -449,6 +451,7 @@ import {
ClockIcon,
FolderOpenIcon,
LayersIcon,
UserIcon,
} from '@modrinth/assets'
import {
ButtonStyled,
@@ -499,7 +502,7 @@ import {
import TimeFramePicker from './TimeframePicker.vue'
const QUERY_BUILDER_DROPDOWN_MAX_HEIGHT = 500
const QUERY_BUILDER_DROPDOWN_MIN_WIDTH = '12rem'
const QUERY_BUILDER_DROPDOWN_MIN_WIDTH = '14rem'
const analyticsQueryChipTriggerClass = 'h-10 '
const analyticsQueryAddFilterButtonClass = '!h-10 max-w-full !w-max !px-3.5 flex !gap-2'
const projectOptionCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
@@ -544,7 +547,7 @@ function getProjectOption(
return {
value: project.id,
label: project.name,
searchTerms: groupTitle ? [groupTitle] : undefined,
searchTerms: [project.id, groupTitle].filter((term): term is string => Boolean(term)),
}
}
@@ -1008,15 +1011,34 @@ const breakdownOptions = computed<MultiSelectOption<Exclude<AnalyticsBreakdownPr
value: 'game_version',
label: formatAnalyticsBreakdownLabel('game_version', formatMessage),
},
{
value: 'dependent_project_download',
label: formatAnalyticsBreakdownLabel('dependent_project_download', formatMessage),
},
{
value: 'user_id',
label: formatAnalyticsBreakdownLabel('user_id', formatMessage),
},
)
return options.map((option) => ({
...option,
disabled: hasReachedBreakdownLimit && !selectedBreakdownSet.has(option.value),
}))
return options.map((option) => {
const isSelected = selectedBreakdownSet.has(option.value)
return {
...option,
disabled:
!isSelected && (hasReachedBreakdownLimit || !canSelectBreakdownOption(option.value)),
}
})
},
)
function canSelectBreakdownOption(breakdown: Exclude<AnalyticsBreakdownPreset, 'none'>): boolean {
return getAnalyticsBreakdownPresetsForProjectSelection(
[...selectedBreakdownValue.value, breakdown],
selectedProjectIds.value,
).includes(breakdown)
}
function getBreakdownOptionLabel(breakdown: Exclude<AnalyticsBreakdownPreset, 'none'>): string {
return (
breakdownOptions.value.find((option) => option.value === breakdown)?.label ??
@@ -1136,6 +1158,17 @@ function withBreakdownFields(
downloads.push('reason')
}
break
case 'user_id':
if (includesStat(breakdownStats, 'revenue') && includesStat(enabledStats, 'revenue')) {
revenue.push('user_id')
}
break
case 'dependent_project_download':
if (includesStat(breakdownStats, 'downloads') && includesStat(enabledStats, 'downloads')) {
downloads.push('dependent_project_id')
downloads.push('reason')
}
break
case 'version_id':
if (includesStat(breakdownStats, 'downloads') && includesStat(enabledStats, 'downloads')) {
downloads.push('version_id')
@@ -1264,6 +1297,12 @@ function buildMetricFilters(
filters.game_version,
),
loader: getFilterValuesForStat('loader_type', 'downloads', enabledStats, filters.loader_type),
dependent_project_id: getFilterValuesForStat(
'dependent_project_id',
'downloads',
enabledStats,
filters.dependent_project_id,
),
},
playtime: {
country: getFilterValuesForStat('country', 'playtime', enabledStats, filters.country),
@@ -1281,7 +1320,9 @@ function buildMetricFilters(
),
loader: getFilterValuesForStat('loader_type', 'playtime', enabledStats, filters.loader_type),
},
revenue: {},
revenue: {
user_id: getFilterValuesForStat('user_id', 'revenue', enabledStats, filters.user_id),
},
}
}
@@ -1295,6 +1336,15 @@ const fetchRequest = computed<Labrinth.Analytics.v3.FetchRequest>(() => {
const bucketBy = withBreakdownFields(selectedBreakdowns.value, selectedFilters.value)
const filterBy = buildMetricFilters(selectedBreakdowns.value, selectedFilters.value)
if (
includesStat(
getEnabledAnalyticsStatsForState(selectedBreakdowns.value, selectedFilters.value),
'revenue',
) &&
!bucketBy.revenue.includes('user_id')
) {
bucketBy.revenue.push('user_id')
}
const filteredProjectIds = getProjectIdsMatchingStatusFilter(
selectedProjectIds.value,
projectStatusById.value,
@@ -2,6 +2,7 @@ import type {
AnalyticsBreakdownPreset,
AnalyticsDashboardStat,
AnalyticsQueryFilterCategory,
AnalyticsSelectedBreakdowns,
AnalyticsSelectedFilters,
} from '~/providers/analytics/analytics'
@@ -13,6 +14,10 @@ export type AnalyticsDashboardDimension =
| 'monetization'
| 'user_agent'
| 'download_reason'
| 'user_id'
| 'dependent_project_download'
| 'dependent_project_id'
| 'dependent_project_type'
| 'game_version'
| 'loader_type'
@@ -23,9 +28,12 @@ export const FILTER_VALUE_CATEGORIES: Exclude<AnalyticsQueryFilterCategory, 'pro
'monetization',
'user_agent',
'download_reason',
'user_id',
'version_id',
'game_version',
'loader_type',
'dependent_project_id',
'dependent_project_type',
]
const ANALYTICS_DASHBOARD_STAT_ORDER: AnalyticsDashboardStat[] = [
@@ -45,8 +53,12 @@ const ANALYTICS_STATS_BY_DIMENSION: Record<
monetization: ['views', 'downloads'],
user_agent: ['downloads'],
download_reason: ['downloads'],
user_id: ['revenue'],
dependent_project_download: ['downloads'],
dependent_project_type: ['downloads'],
game_version: ['downloads', 'playtime'],
loader_type: ['downloads', 'playtime'],
dependent_project_id: ['downloads'],
project_status: ANALYTICS_DASHBOARD_STAT_ORDER,
}
@@ -60,6 +72,8 @@ const ANALYTICS_DIMENSION_BY_BREAKDOWN: Record<
monetization: 'monetization',
user_agent: 'user_agent',
download_reason: 'download_reason',
user_id: 'user_id',
dependent_project_download: 'dependent_project_download',
version_id: 'version_id',
loader: 'loader_type',
game_version: 'game_version',
@@ -74,9 +88,12 @@ const ANALYTICS_DIMENSION_BY_FILTER_CATEGORY: Record<
monetization: 'monetization',
user_agent: 'user_agent',
download_reason: 'download_reason',
user_id: 'user_id',
version_id: 'version_id',
game_version: 'game_version',
loader_type: 'loader_type',
dependent_project_id: 'dependent_project_id',
dependent_project_type: 'dependent_project_type',
}
const ANALYTICS_FILTER_CATEGORY_BY_BREAKDOWN: Record<
@@ -89,6 +106,8 @@ const ANALYTICS_FILTER_CATEGORY_BY_BREAKDOWN: Record<
monetization: 'monetization',
user_agent: 'user_agent',
download_reason: 'download_reason',
user_id: 'user_id',
dependent_project_download: null,
version_id: 'version_id',
loader: 'loader_type',
game_version: 'game_version',
@@ -286,6 +305,33 @@ export function getAnalyticsStatsForBreakdowns(
return stats
}
export function getAnalyticsBreakdownsWithSharedStats(
breakdowns: AnalyticsBreakdownInput,
): AnalyticsSelectedBreakdowns {
const normalizedBreakdowns = normalizeAnalyticsBreakdowns(breakdowns)
const compatibleBreakdowns: AnalyticsSelectedBreakdowns = []
let sharedStats: readonly AnalyticsDashboardStat[] | null = null
for (const breakdown of normalizedBreakdowns) {
const breakdownStats = getAnalyticsStatsForBreakdown(breakdown)
if (sharedStats === null) {
compatibleBreakdowns.push(breakdown)
sharedStats = breakdownStats
continue
}
const nextSharedStats = intersectAnalyticsStats(sharedStats, breakdownStats)
if (nextSharedStats.length === 0) {
continue
}
compatibleBreakdowns.push(breakdown)
sharedStats = nextSharedStats
}
return compatibleBreakdowns
}
export function getAnalyticsStatsForFilterCategory(
category: AnalyticsQueryFilterCategory,
): readonly AnalyticsDashboardStat[] {
@@ -331,12 +377,20 @@ export function getVisibleAnalyticsFilterCategoriesForState(
breakdowns: AnalyticsBreakdownInput,
filters: AnalyticsSelectedFilters,
): readonly Exclude<AnalyticsQueryFilterCategory, 'project'>[] {
return FILTER_VALUE_CATEGORIES.filter((category) =>
haveAnalyticsStatOverlap(
const normalizedBreakdowns = normalizeAnalyticsBreakdowns(breakdowns)
return FILTER_VALUE_CATEGORIES.filter((category) => {
if (
category === 'dependent_project_type' &&
!normalizedBreakdowns.includes('dependent_project_download')
) {
return false
}
return haveAnalyticsStatOverlap(
getAnalyticsStatsForFilterScope(breakdowns, filters, category),
getAnalyticsStatsForFilterCategory(category),
),
)
)
})
}
export function sanitizeAnalyticsSelectedFilters(
@@ -345,6 +399,7 @@ export function sanitizeAnalyticsSelectedFilters(
): AnalyticsSelectedFilters {
const nextFilters = cloneSelectedFilters(filters)
let availableStats = [...getAnalyticsStatsForBreakdowns(breakdowns)]
const normalizedBreakdowns = normalizeAnalyticsBreakdowns(breakdowns)
for (const category of FILTER_VALUE_CATEGORIES) {
if (filters[category].length === 0) {
@@ -352,7 +407,11 @@ export function sanitizeAnalyticsSelectedFilters(
}
const categoryStats = getAnalyticsStatsForFilterCategory(category)
if (!haveAnalyticsStatOverlap(availableStats, categoryStats)) {
if (
!haveAnalyticsStatOverlap(availableStats, categoryStats) ||
(category === 'dependent_project_type' &&
!normalizedBreakdowns.includes('dependent_project_download'))
) {
nextFilters[category] = []
continue
}
@@ -371,9 +430,12 @@ export function cloneSelectedFilters(filters: AnalyticsSelectedFilters): Analyti
monetization: [...filters.monetization],
user_agent: [...filters.user_agent],
download_reason: [...filters.download_reason],
user_id: [...filters.user_id],
version_id: [...filters.version_id],
game_version: [...filters.game_version],
loader_type: [...filters.loader_type],
dependent_project_id: [...filters.dependent_project_id],
dependent_project_type: [...filters.dependent_project_type],
}
}
@@ -456,7 +518,7 @@ export function normalizeSelectedValues(
.map((value) => value.trim().toLowerCase())
.filter(isProjectStatusFilterValue)
}
if (categoryKey === 'loader_type') {
if (categoryKey === 'loader_type' || categoryKey === 'dependent_project_type') {
return Array.from(
new Set(
selectedValues
@@ -6,6 +6,7 @@ import {
areStringArraysEqual,
buildAnalyticsQueryBuilderRouteQuery,
getAnalyticsBreakdownPresetsForProjectSelection,
hasAnalyticsAllProjectSelectionQuery,
hasAnalyticsQueryBuilderRouteChange,
readAnalyticsGraphState,
readAnalyticsQueryBuilderState,
@@ -54,6 +55,7 @@ export interface UseAnalyticsRouteSyncOptions {
graph: AnalyticsGraphRefs
availableProjectIds: Ref<string[]>
defaultProjectIds: Ref<string[]>
areProjectsLoaded: Ref<boolean>
sanitizeSelectedFilters: (
breakdowns: readonly AnalyticsBreakdownPreset[],
filters: AnalyticsSelectedFilters,
@@ -61,8 +63,14 @@ export interface UseAnalyticsRouteSyncOptions {
}
export function useAnalyticsRouteSync(options: UseAnalyticsRouteSyncOptions) {
const { queryBuilder, graph, availableProjectIds, defaultProjectIds, sanitizeSelectedFilters } =
options
const {
queryBuilder,
graph,
availableProjectIds,
defaultProjectIds,
areProjectsLoaded,
sanitizeSelectedFilters,
} = options
const route = useRoute()
const router = useRouter()
@@ -113,6 +121,10 @@ export function useAnalyticsRouteSync(options: UseAnalyticsRouteSyncOptions) {
return
}
if (hasAnalyticsAllProjectSelectionQuery(route.query) && !areProjectsLoaded.value) {
return
}
const nextRouteQuery = buildAnalyticsQueryBuilderRouteQuery(
route.query,
getSelectedAnalyticsQueryBuilderState(),
@@ -147,6 +159,10 @@ export function useAnalyticsRouteSync(options: UseAnalyticsRouteSyncOptions) {
}
function applyRouteQueryToState(nextQuery: LocationQuery) {
if (hasAnalyticsAllProjectSelectionQuery(nextQuery) && !areProjectsLoaded.value) {
return
}
const nextQueryState = readAnalyticsQueryBuilderState(
nextQuery,
availableProjectIds.value,
@@ -0,0 +1,271 @@
<template>
<div
class="shadow-card mx-auto flex w-full max-w-[27rem] flex-col gap-6 rounded-2xl border border-solid border-button-bg bg-surface-3 !p-6"
>
<h1
class="mx-auto my-0 flex w-full justify-center text-center text-2xl font-semibold text-contrast"
>
{{ formatMessage(messages.title) }}
</h1>
<section v-if="requiresDob" class="flex flex-col gap-2.5">
<label class="text-md font-semibold text-contrast" for="create-account-dob">
{{ formatMessage(messages.dateOfBirthLabel) }}
</label>
<DatePicker
id="create-account-dob"
v-model="dateOfBirthModel"
wrapper-class="w-full"
min-date="1900-01-01"
:max-date="maxInputDate"
:default-view-date="defaultDobViewDate"
preserve-day
:placeholder="formatMessage(messages.dateOfBirthPlaceholder)"
/>
<Admonition :type="'info'">
<template #header>
<div class="-mb-3 -mt-1 flex flex-col gap-0 text-sm font-normal leading-normal">
<div>
{{ formatMessage(messages.infoPanelText) }}
</div>
<a
class="w-fit text-link underline"
:href="SOURCE_CODE_URL"
target="_blank"
rel="noopener noreferrer"
>
{{ formatMessage(messages.relevantSourceCodeText) }}
</a>
</div>
</template>
</Admonition>
</section>
<section class="flex flex-col gap-2.5">
<label class="text-md font-semibold text-contrast" for="create-account-username">
{{ formatMessage(messages.usernameOptionalLabel) }}
</label>
<StyledInput
id="create-account-username"
v-model="usernameModel"
type="text"
:placeholder="formatMessage(messages.usernamePlaceholder)"
wrapper-class="w-full"
/>
</section>
<section v-if="globals?.captcha_enabled" class="flex flex-col gap-2.5">
<label class="text-md font-semibold text-contrast">{{
formatMessage(messages.securityCheckLabel)
}}</label>
<HCaptcha v-if="globals?.captcha_enabled" :ref="onSetCaptchaRef" v-model="tokenModel" />
</section>
<div
class="flex gap-2.5 rounded-2xl border border-solid border-surface-5 bg-surface-3 transition-all hover:brightness-110"
>
<Checkbox
v-model="subscribeModel"
class="p-3 text-left leading-snug text-primary transition-all"
:label="formatMessage(messages.subscribeLabel)"
:description="formatMessage(messages.subscribeLabel)"
/>
</div>
<ButtonStyled color="brand">
<button
class="!w-full font-bold"
:disabled="globals?.captcha_enabled ? !tokenModel : false"
@click="onCompleteSignUpClick"
>
{{ formatMessage(messages.completeSignUpButton) }}
<RightArrowIcon />
</button>
</ButtonStyled>
</div>
</template>
<script setup lang="ts">
import { RightArrowIcon } from '@modrinth/assets'
import {
Admonition,
ButtonStyled,
Checkbox,
DatePicker,
defineMessages,
injectNotificationManager,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { computed } from 'vue'
import HCaptcha from '@/components/ui/auth/HCaptcha.vue'
interface AuthGlobals {
captcha_enabled?: boolean
[key: string]: unknown
}
interface Props {
globals?: AuthGlobals | null
requiresDob?: boolean
onCompleteSignUp?: () => void
onSetCaptchaRef?: ((captchaRef: unknown) => void) | undefined
}
const {
globals = null,
requiresDob = true,
onCompleteSignUp = () => {},
onSetCaptchaRef = undefined,
} = defineProps<Props>()
const SOURCE_CODE_URL =
'https://github.com/modrinth/code/blob/main/apps/frontend/src/components/ui/auth/CreateAccount.vue'
const dateOfBirthModel = defineModel<string | null>('dateOfBirth', { default: '' })
const usernameModel = defineModel<string>('username', { default: '' })
const tokenModel = defineModel<string>('token', { default: '' })
const subscribeModel = defineModel<boolean>('subscribe', { default: false })
const maxInputDate = computed(() => `${new Date().getFullYear()}-12-31`)
const defaultDobViewDate = computed(() => `${new Date().getFullYear()}-01-01`)
const maxBirthDate = computed(() => {
const date = new Date()
date.setFullYear(date.getFullYear() - 13)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
})
const getBirthYear = (dateOfBirth: string | null): number | null => {
if (!dateOfBirth) {
return null
}
const [yearPart = ''] = dateOfBirth.split('-')
const year = Number(yearPart)
return Number.isInteger(year) ? year : null
}
const isDateOfBirthMissing = computed(() => requiresDob && !dateOfBirthModel.value)
const isDateOfBirthYearZero = computed(() => {
if (!requiresDob || !dateOfBirthModel.value) {
return false
}
return getBirthYear(dateOfBirthModel.value) === 0
})
const isUnder13 = computed(
() => requiresDob && !!dateOfBirthModel.value && dateOfBirthModel.value > maxBirthDate.value,
)
const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl()
function onCompleteSignUpClick() {
if (isDateOfBirthMissing.value) {
addNotification({
title: formatMessage(messages.dateOfBirthRequiredTitle),
text: formatMessage(messages.dateOfBirthRequiredText),
type: 'warning',
})
return
}
if (isDateOfBirthYearZero.value) {
addNotification({
title: formatMessage(messages.dateOfBirthInvalidTitle),
text: formatMessage(messages.dateOfBirthInvalidText),
type: 'error',
})
return
}
if (isUnder13.value) {
addNotification({
title: formatMessage(messages.ageRequirementWarningTitle),
text: formatMessage(messages.under13HelperText),
type: 'error',
})
return
}
onCompleteSignUp()
}
const messages = defineMessages({
title: {
id: 'auth.create-account.title',
defaultMessage: 'Create an Account',
},
dateOfBirthLabel: {
id: 'auth.create-account.date-of-birth.label',
defaultMessage: 'Date of birth',
},
dateOfBirthPlaceholder: {
id: 'auth.create-account.date-of-birth.placeholder',
defaultMessage: 'Select your date of birth',
},
dateOfBirthRequiredTitle: {
id: 'auth.create-account.date-of-birth.required.title',
defaultMessage: 'Date of birth required',
},
dateOfBirthRequiredText: {
id: 'auth.create-account.date-of-birth.required.text',
defaultMessage: 'Please enter your date of birth before continuing.',
},
dateOfBirthInvalidTitle: {
id: 'auth.create-account.date-of-birth.invalid.title',
defaultMessage: 'Invalid date of birth',
},
dateOfBirthInvalidText: {
id: 'auth.create-account.date-of-birth.invalid.text',
defaultMessage: 'Please enter a valid date of birth. Year cannot be 0000.',
},
under13HelperText: {
id: 'auth.create-account.date-of-birth.under13-helper',
defaultMessage: 'You do not meet the age requirement to create an account at Modrinth.',
},
ageRequirementWarningTitle: {
id: 'auth.create-account.age-requirement.warning-title',
defaultMessage: 'Age requirement',
},
infoPanelText: {
id: 'auth.create-account.info-panel.text',
defaultMessage:
'We do not store your date of birth. We only use it during sign-up to validate COPPA compliance.',
},
relevantSourceCodeText: {
id: 'auth.create-account.info-panel.source-code-link',
defaultMessage: 'Relevant source code',
},
usernameOptionalLabel: {
id: 'auth.create-account.username.label',
defaultMessage: 'Username',
},
usernamePlaceholder: {
id: 'auth.create-account.username.placeholder',
defaultMessage: 'Enter username',
},
securityCheckLabel: {
id: 'auth.create-account.security-check.label',
defaultMessage: 'Security check',
},
subscribeLabel: {
id: 'auth.create-account.subscribe.label',
defaultMessage: 'Keep me updated on the cool things Modrinth is working on via email',
},
completeSignUpButton: {
id: 'auth.create-account.complete-sign-up',
defaultMessage: 'Complete sign up',
},
})
</script>
@@ -47,17 +47,34 @@ defineExpose({
></div>
</template>
<style lang="scss">
<style>
.h-captcha {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: var(--radius-md);
border: 2px solid var(--color-button-bg);
height: 78px;
height: 100px;
width: 100%;
max-width: 100%;
}
iframe {
margin: -1px;
.h-captcha iframe {
transform: scale(1.33);
transform-origin: center;
margin: -1px;
}
@media screen and (max-width: 400px) {
.h-captcha {
height: auto;
}
.h-captcha iframe {
transform: scale(1.03);
margin: 0;
max-width: 100%;
}
}
</style>
@@ -0,0 +1,416 @@
<template>
<div class="contents">
<ConfirmModal
ref="removePasskeyModal"
:title="formatMessage(messages.removePasskeyConfirmTitle)"
:description="
formatMessage(messages.removePasskeyConfirmDescription, { name: passkeyToRemove?.name })
"
:proceed-label="formatMessage(commonMessages.removeButton)"
@proceed="removePasskey()"
/>
<NewModal
ref="managePasskeyModal"
width="600px"
:header="formatMessage(messages.managePasskeyTitle)"
>
<div class="flex flex-col gap-4">
<div v-if="passkeysLoading" class="flex items-center gap-2 text-secondary">
<SpinnerIcon class="animate-spin" />
{{ formatMessage(messages.managePasskeyModalLoading) }}
</div>
<template v-else>
<p v-if="passkeys.length === 0" class="m-0 text-secondary">
{{ formatMessage(messages.managePasskeyModalNoPasskeys) }}
</p>
<div
v-for="passkey in passkeys"
:key="passkey.id"
class="universal-card recessed flex items-center justify-between"
>
<div>
<div>
<strong>{{ passkey.name }}</strong>
</div>
<div>
<span v-tooltip="formatDateTime(passkey.created_at)">
{{
formatMessage(messages.managePasskeyModalAdded, {
ago: formatRelativeTime(passkey.created_at),
})
}}
</span>
<span v-if="passkey.last_used" v-tooltip="formatDateTime(passkey.last_used)">
{{
formatMessage(messages.managePasskeyModalLastUsed, {
ago: formatRelativeTime(passkey.last_used),
})
}}
</span>
<span v-else>
{{ formatMessage(messages.managePasskeyModalNeverUsed) }}
</span>
</div>
</div>
<div class="flex gap-2">
<ButtonStyled circular>
<button
v-tooltip="formatMessage(commonMessages.renameButton)"
@click="
() => {
passkeyToRename = { ...passkey }
renamePasskeyModal?.show()
}
"
>
<EditIcon />
</button>
</ButtonStyled>
<ButtonStyled circular>
<button
v-tooltip="formatMessage(commonMessages.removeButton)"
@click="
() => {
passkeyToRemove = passkey
removePasskeyModal?.show()
}
"
>
<TrashIcon />
</button>
</ButtonStyled>
</div>
</div>
</template>
<div class="input-group self-end">
<ButtonStyled>
<button @click="registerPasskey()">
<PlusIcon />
{{ formatMessage(messages.managePasskeyAddPasskey) }}
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="managePasskeyModal?.hide()">
<XIcon />
{{ formatMessage(commonMessages.closeButton) }}
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<NewModal
ref="addPasskeyModal"
width="500px"
:header="formatMessage(messages.managePasskeyAddPasskey)"
>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<div class="text-md font-semibold text-contrast">
{{ formatMessage(messages.passkeyNameLabel) }}
</div>
<StyledInput
id="passkey-name"
v-model="pendingPasskeyName"
:maxlength="255"
type="text"
:placeholder="formatMessage(messages.passkeyNamePlaceholder)"
@keyup.enter="pendingPasskeyName && finishRegisterPasskey()"
/>
<div class="label__description">
{{ formatMessage(messages.passkeyNameDescription) }}
</div>
</div>
<div class="flex justify-end gap-2.5">
<ButtonStyled>
<button @click="addPasskeyModal?.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button :disabled="!pendingPasskeyName" @click="finishRegisterPasskey()">
<PlusIcon />
{{ formatMessage(messages.managePasskeyAddPasskey) }}
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<NewModal
ref="renamePasskeyModal"
width="500px"
:header="formatMessage(messages.renamePasskeyModalHeader)"
>
<div v-if="passkeyToRename" class="flex flex-col gap-6">
<div class="flex flex-col gap-2.5">
<div class="text-md font-semibold text-contrast">
{{ formatMessage(messages.passkeyNameLabel) }}
</div>
<StyledInput
id="passkey-rename"
v-model="passkeyToRenameName"
:maxlength="255"
type="text"
:placeholder="formatMessage(messages.passkeyNamePlaceholder)"
@keyup.enter="passkeyToRenameName && renamePasskey()"
/>
<div class="label__description">
{{ formatMessage(messages.passkeyNameDescription) }}
</div>
</div>
<div class="flex justify-end gap-2.5">
<ButtonStyled>
<button @click="renamePasskeyModal?.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button :disabled="!passkeyToRenameName" @click="renamePasskey()">
<SaveIcon />
{{ formatMessage(commonMessages.saveButton) }}
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<div class="adjacent-input mb-0">
<label for="manage-passkeys">
<span class="label__title">{{ formatMessage(messages.managePasskeyTitle) }}</span>
<span class="label__description">{{
formatMessage(messages.managePasskeyDescription)
}}</span>
</label>
<div>
<ButtonStyled>
<button id="manage-passkeys" @click="showPasskeyModal">
<UserKeyIcon /> {{ formatMessage(messages.managePasskeyTitle) }}
</button>
</ButtonStyled>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
EditIcon,
PlusIcon,
SaveIcon,
SpinnerIcon,
TrashIcon,
UserKeyIcon,
XIcon,
} from '@modrinth/assets'
import {
ButtonStyled,
commonMessages,
ConfirmModal,
defineMessages,
injectModrinthClient,
injectNotificationManager,
NewModal,
StyledInput,
useFormatDateTime,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { computed, ref, useTemplateRef } from 'vue'
import { createPasskeyCredential } from '~/helpers/passkey.ts'
const { labrinth } = injectModrinthClient()
const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
function notifyError(err: unknown) {
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
text: err instanceof Error ? err.message : String(err),
type: 'error',
})
}
const messages = defineMessages({
managePasskeyTitle: {
id: 'settings.account.security.passkey.title',
defaultMessage: 'Manage passkeys',
},
managePasskeyDescription: {
id: 'settings.account.security.passkey.description',
defaultMessage: 'Manage your registered passkeys, or add a new one.',
},
managePasskeyAddPasskey: {
id: 'settings.account.security.passkey.add',
defaultMessage: 'Add passkey',
},
managePasskeyModalLoading: {
id: 'settings.account.security.passkey.modal.loading',
defaultMessage: 'Loading passkeys…',
},
managePasskeyModalNoPasskeys: {
id: 'settings.account.security.passkey.modal.no-passkeys',
defaultMessage: 'You do not have any passkeys registered.',
},
managePasskeyModalAdded: {
id: 'settings.account.security.passkey.modal.added',
defaultMessage: 'Added {ago}',
},
managePasskeyModalLastUsed: {
id: 'settings.account.security.passkey.modal.last-used',
defaultMessage: 'Last used {ago}',
},
managePasskeyModalNeverUsed: {
id: 'settings.account.security.passkey.modal.never-used',
defaultMessage: 'Never used',
},
passkeyNameLabel: {
id: 'settings.account.security.passkey.add-modal.name.label',
defaultMessage: 'Name',
},
passkeyNameDescription: {
id: 'settings.account.security.passkey.add-modal.name.description',
defaultMessage:
'Make sure to pick something memorable, so you can identify this passkey later.',
},
passkeyNamePlaceholder: {
id: 'settings.account.security.passkey.add-modal.name.placeholder',
defaultMessage: 'My passkey',
},
renamePasskeyModalHeader: {
id: 'settings.account.security.passkey.rename-modal.header',
defaultMessage: 'Rename passkey',
},
removePasskeyConfirmTitle: {
id: 'settings.account.security.passkey.remove.title',
defaultMessage: 'Are you sure you want to remove this passkey?',
},
removePasskeyConfirmDescription: {
id: 'settings.account.security.passkey.remove.description',
defaultMessage:
'This will permanently remove the passkey "{name}". You will no longer be able to sign in with it.',
},
})
const removePasskeyModal = useTemplateRef<InstanceType<typeof ConfirmModal>>('removePasskeyModal')
const managePasskeyModal = useTemplateRef<InstanceType<typeof NewModal>>('managePasskeyModal')
const addPasskeyModal = useTemplateRef<InstanceType<typeof NewModal>>('addPasskeyModal')
const renamePasskeyModal = useTemplateRef<InstanceType<typeof NewModal>>('renamePasskeyModal')
const pendingPasskey = ref<Labrinth.Auth.v2.PasskeyRegisterFinishRequest | null>(null)
const passkeyToRemove = ref<Labrinth.Auth.v2.Passkey | null>(null)
const passkeyToRename = ref<Labrinth.Auth.v2.Passkey | null>(null)
const pendingPasskeyName = computed({
get: () => pendingPasskey.value?.name ?? '',
set: (name) => {
if (pendingPasskey.value) {
pendingPasskey.value.name = name
}
},
})
const passkeyToRenameName = computed({
get: () => passkeyToRename.value?.name ?? '',
set: (name) => {
if (passkeyToRename.value) {
passkeyToRename.value.name = name
}
},
})
const passkeys = ref<Labrinth.Auth.v2.Passkey[]>([])
const passkeysLoading = ref(false)
async function fetchPasskeys() {
passkeysLoading.value = true
try {
passkeys.value = await labrinth.auth_v2.listPasskeys()
} catch (err) {
notifyError(err)
}
passkeysLoading.value = false
}
async function showPasskeyModal() {
managePasskeyModal.value?.show()
await fetchPasskeys()
}
async function registerPasskey() {
startLoading()
try {
const res = await labrinth.auth_v2.registerPasskeyStart()
const credential = await createPasskeyCredential(res.options.publicKey)
pendingPasskey.value = {
flow: res.flow,
credential: credential,
name: '',
}
addPasskeyModal.value?.show()
} catch (err) {
notifyError(err)
}
stopLoading()
}
async function finishRegisterPasskey() {
if (!pendingPasskey.value) return
startLoading()
try {
const passkey = await labrinth.auth_v2.registerPasskeyFinish(pendingPasskey.value)
passkeys.value.unshift(passkey)
pendingPasskey.value = null
addPasskeyModal.value?.hide()
} catch (err) {
notifyError(err)
}
stopLoading()
}
async function renamePasskey() {
if (!passkeyToRename.value) return
const { id, name } = passkeyToRename.value
startLoading()
try {
await labrinth.auth_v2.renamePasskey(id, { name })
const existing = passkeys.value.find((passkey) => passkey.id === id)
if (existing) {
existing.name = name
}
passkeyToRename.value = null
renamePasskeyModal.value?.hide()
} catch (err) {
notifyError(err)
}
stopLoading()
}
async function removePasskey() {
if (!passkeyToRemove.value) return
const { id } = passkeyToRemove.value
startLoading()
try {
await labrinth.auth_v2.deletePasskey(id)
passkeys.value = passkeys.value.filter((passkey) => passkey.id !== id)
passkeyToRemove.value = null
} catch (err) {
notifyError(err)
}
stopLoading()
}
</script>
@@ -0,0 +1,319 @@
<template>
<div v-if="subtleLauncherRedirectUri">
<iframe
:src="subtleLauncherRedirectUri"
class="fixed left-0 top-0 z-[9999] m-0 h-full w-full border-0 p-0"
></iframe>
</div>
<div
v-else
class="universal-card mx-auto flex w-full max-w-[27rem] flex-col gap-6 border border-solid border-surface-5 !p-6"
>
<template v-if="flow && !subtleLauncherRedirectUri">
<div class="flex flex-col items-end gap-4">
<div class="flex flex-col gap-1.5">
<label for="two-factor-code">
<span class="label__title">{{ formatMessage(messages.twoFactorCodeLabel) }}</span>
<span class="label__description">
{{ formatMessage(messages.twoFactorCodeLabelDescription) }}
</span>
</label>
<StyledInput
id="two-factor-code"
v-model="twoFactorCodeModel"
:maxlength="11"
inputmode="numeric"
:placeholder="formatMessage(messages.twoFactorCodeInputPlaceholder)"
autocomplete="one-time-code"
@keyup.enter="onTwoFactorSignIn()"
/>
</div>
<ButtonStyled color="brand">
<button @click="onTwoFactorSignIn()">
{{ formatMessage(commonMessages.signInButton) }} <RightArrowIcon />
</button>
</ButtonStyled>
</div>
</template>
<template v-else>
<div class="flex flex-col gap-5">
<div class="text-center text-2xl font-semibold text-contrast">
{{ formatMessage(messages.signInWithLabel) }}
</div>
<section class="grid grid-cols-1 gap-2.5 sm:grid-cols-2">
<ButtonStyled v-for="provider in oauthProviders" :key="provider.id">
<a
class="relative w-full !justify-center overflow-visible !shadow-none"
:class="{
'!border !border-[var(--color-green)]': lastSignInProvider === provider.id,
}"
:href="getAuthUrl(provider.id, redirectTarget)"
:aria-label="
formatMessage(messages.continueWithProvider, { provider: provider.name })
"
@click="onOAuthProviderClick(provider.id)"
>
<component :is="provider.icon" />
<span>{{ provider.name }}</span>
<span
v-if="lastSignInProvider === provider.id"
class="oauth-provider-last-sign-in-badge"
>
{{ formatMessage(messages.lastSignInLabel) }}
</span>
</a>
</ButtonStyled>
<ButtonStyled>
<a
class="relative w-full !justify-center overflow-visible !shadow-none sm:col-span-2"
:class="{ '!border !border-[var(--color-green)]': lastSignInProvider === 'passkey' }"
role="button"
tabindex="0"
@click="onPasskeySignIn"
@keydown.enter="onPasskeySignIn"
>
<UserKeyIcon />
<span class="ml-1">{{ formatMessage(messages.continueWithPasskey) }}</span>
<span
v-if="lastSignInProvider === 'passkey'"
class="oauth-provider-last-sign-in-badge"
>
{{ formatMessage(messages.lastSignInLabel) }}
</span>
</a>
</ButtonStyled>
</section>
<div class="h-px w-full bg-surface-5"></div>
<section class="mx-auto flex w-full flex-col gap-2.5">
<label for="email" hidden>{{ formatMessage(commonMessages.emailUsernameLabel) }}</label>
<StyledInput
id="email"
v-model="emailModel"
:icon="MailIcon"
type="text"
inputmode="email"
autocomplete="username"
:placeholder="formatMessage(commonMessages.emailUsernameLabel)"
wrapper-class="w-full"
/>
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
<StyledInput
id="password"
v-model="passwordModel"
:icon="KeyIcon"
type="password"
autocomplete="current-password"
:placeholder="formatMessage(commonMessages.passwordLabel)"
wrapper-class="w-full"
/>
<HCaptcha
v-if="globals?.captcha_enabled && emailModel && passwordModel"
:ref="onSetCaptchaRef"
v-model="tokenModel"
/>
<ButtonStyled color="brand">
<button
class="!w-full"
:disabled="globals?.captcha_enabled ? !tokenModel : false"
@click="onPasswordSignIn()"
>
{{ formatMessage(messages.continueWithEmail) }} <RightArrowIcon />
</button>
</ButtonStyled>
<div class="flex flex-wrap items-center justify-center gap-2.5 !text-base">
<NuxtLink
class="text-link"
:to="{
path: '/auth/reset-password',
query: routeQuery,
}"
>
{{ formatMessage(messages.forgotPasswordLabel) }}
</NuxtLink>
<div class="h-1.5 w-1.5 rounded-full bg-surface-5" />
<NuxtLink
class="inline text-link"
:to="{
path: '/auth/sign-up',
query: routeQuery,
}"
>
{{ formatMessage(messages.createAccountLabel) }}
</NuxtLink>
</div>
</section>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import {
DiscordColorIcon,
GitHubColorIcon,
GitLabColorIcon,
GoogleColorIcon,
KeyIcon,
MailIcon,
MicrosoftColorIcon,
RightArrowIcon,
SteamColorIcon,
UserKeyIcon,
} from '@modrinth/assets'
import { ButtonStyled, commonMessages, defineMessages, StyledInput, useVIntl } from '@modrinth/ui'
import { useStorage } from '@vueuse/core'
import { computed } from 'vue'
import type { LocationQuery } from 'vue-router'
import HCaptcha from '@/components/ui/auth/HCaptcha.vue'
import {
getAuthUrl,
LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
} from '@/composables/auth.ts'
const oauthProviders = [
{ id: 'discord', name: 'Discord', icon: DiscordColorIcon },
{ id: 'github', name: 'GitHub', icon: GitHubColorIcon },
{ id: 'microsoft', name: 'Microsoft', icon: MicrosoftColorIcon },
{ id: 'google', name: 'Google', icon: GoogleColorIcon },
{ id: 'steam', name: 'Steam', icon: SteamColorIcon },
{ id: 'gitlab', name: 'GitLab', icon: GitLabColorIcon },
] as const
type AuthProvider = (typeof oauthProviders)[number]['id'] | 'passkey'
interface AuthGlobals {
captcha_enabled?: boolean
[key: string]: unknown
}
interface Props {
subtleLauncherRedirectUri?: string
flow?: string
redirectTarget?: string
routeQuery?: LocationQuery
globals?: AuthGlobals | null
onPasswordSignIn?: () => void
onTwoFactorSignIn?: () => void
onPasskeySignIn?: () => void
onSetCaptchaRef?: ((captchaRef: unknown) => void) | undefined
}
const {
subtleLauncherRedirectUri = '',
flow = '',
redirectTarget = '',
routeQuery = {},
globals = null,
onPasswordSignIn = () => {},
onTwoFactorSignIn = () => {},
onPasskeySignIn = () => {},
onSetCaptchaRef = undefined,
} = defineProps<Props>()
const emailModel = defineModel<string>('email', { default: '' })
const passwordModel = defineModel<string>('password', { default: '' })
const tokenModel = defineModel<string>('token', { default: '' })
const twoFactorCodeModel = defineModel<string>('twoFactorCode', { default: '' })
const lastSignInOAuthProvider = useStorage<AuthProvider | null>(
LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
null,
undefined,
{ initOnMounted: true },
)
const pendingSignInOAuthProvider = useStorage<AuthProvider | null>(
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
null,
undefined,
{ initOnMounted: true },
)
const lastSignInProvider = computed(() => lastSignInOAuthProvider.value)
const onOAuthProviderClick = (provider: AuthProvider) => {
pendingSignInOAuthProvider.value = provider
}
const { formatMessage } = useVIntl()
const messages = defineMessages({
forgotPasswordLabel: {
id: 'auth.sign-in.forgot-password',
defaultMessage: 'Forgot password',
},
noAccountLabel: {
id: 'auth.sign-in.no-account',
defaultMessage: "Don't have an account?",
},
createAccountLabel: {
id: 'auth.sign-in.create-account',
defaultMessage: 'Sign up',
},
signInWithLabel: {
id: 'auth.sign-in.sign-in-with',
defaultMessage: 'Sign into Modrinth',
},
twoFactorCodeInputPlaceholder: {
id: 'auth.sign-in.2fa.placeholder',
defaultMessage: 'Enter code...',
},
twoFactorCodeLabel: {
id: 'auth.sign-in.2fa.label',
defaultMessage: 'Two-factor authentication',
},
twoFactorCodeLabelDescription: {
id: 'auth.sign-in.2fa.description',
defaultMessage:
'Enter the 6-digit code from your authenticator app, or one of your backup codes.',
},
continueWithProvider: {
id: 'auth.continue-with-provider',
defaultMessage: 'Continue with {provider}',
},
continueWithEmail: {
id: 'auth.sign-in.continue-with-email',
defaultMessage: 'Continue with Email',
},
lastSignInLabel: {
id: 'auth.sign-in.last-sign-in',
defaultMessage: 'Last used',
},
continueWithPasskey: {
id: 'auth.sign-in.continue-with-passkey',
defaultMessage: 'Continue with passkey',
},
})
</script>
<style scoped lang="scss">
.oauth-provider-last-sign-in-badge {
position: absolute;
top: -0.75rem;
right: 0.25rem;
z-index: 1;
border-radius: 9999px;
background-color: var(--surface-3);
color: var(--color-green);
border: 1px solid var(--color-green);
padding: 0.125rem 0.375rem;
font-size: 0.75rem;
font-weight: 600;
line-height: 1;
pointer-events: none;
}
.oauth-provider-last-sign-in-badge::before {
content: '';
inset: 0;
border-radius: inherit;
background-color: var(--color-green-highlight);
position: absolute;
z-index: 0;
}
</style>
@@ -0,0 +1,236 @@
<template>
<div
class="universal-card flex w-full max-w-[27rem] flex-col gap-6 border border-solid border-surface-5 !p-6"
>
<div class="mx-auto text-center text-2xl font-semibold text-contrast">
{{ formatMessage(messages.signUpWithTitle) }}
</div>
<section class="flex flex-col gap-2.5">
<ButtonStyled>
<a
class="!shadow-none"
:href="getAuthUrl('google', redirectTarget)"
@click="onOAuthProviderClick('google')"
>
<GoogleColorIcon />
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'Google' }) }}</span>
</a>
</ButtonStyled>
<ButtonStyled>
<a
class="!shadow-none"
:href="getAuthUrl('microsoft', redirectTarget)"
@click="onOAuthProviderClick('microsoft')"
>
<MicrosoftColorIcon />
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'Microsoft' }) }}</span>
</a>
</ButtonStyled>
<ButtonStyled>
<a
class="!shadow-none"
:href="getAuthUrl('discord', redirectTarget)"
@click="onOAuthProviderClick('discord')"
>
<DiscordColorIcon />
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'Discord' }) }}</span>
</a>
</ButtonStyled>
<template v-if="showOtherOptions">
<ButtonStyled>
<a
class="!shadow-none"
:href="getAuthUrl('github', redirectTarget)"
@click="onOAuthProviderClick('github')"
>
<GitHubColorIcon />
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'GitHub' }) }}</span>
</a>
</ButtonStyled>
<ButtonStyled>
<a
class="!shadow-none"
:href="getAuthUrl('gitlab', redirectTarget)"
@click="onOAuthProviderClick('gitlab')"
>
<GitLabColorIcon />
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'GitLab' }) }}</span>
</a>
</ButtonStyled>
<ButtonStyled>
<a
class="!shadow-none"
:href="getAuthUrl('steam', redirectTarget)"
@click="onOAuthProviderClick('steam')"
>
<SteamColorIcon />
<span>{{ formatMessage(messages.continueWithProvider, { provider: 'Steam' }) }}</span>
</a>
</ButtonStyled>
</template>
<button
class="mx-auto -mb-3 bg-transparent pt-1 text-center text-base font-semibold text-secondary transition-all hover:text-primary"
@click="onToggleOtherOptions()"
>
{{
showOtherOptions
? formatMessage(messages.showFewerOptions)
: formatMessage(messages.showOtherOptions)
}}
</button>
</section>
<div class="h-px w-full bg-surface-5"></div>
<section class="flex flex-col gap-2.5">
<label for="email" hidden>{{ formatMessage(commonMessages.emailLabel) }}</label>
<StyledInput
id="email"
v-model="emailModel"
:icon="MailIcon"
type="email"
autocomplete="email"
:placeholder="formatMessage(commonMessages.emailLabel)"
wrapper-class="w-full"
/>
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
<StyledInput
id="password"
v-model="passwordModel"
:icon="KeyIcon"
type="password"
autocomplete="new-password"
:placeholder="formatMessage(commonMessages.passwordLabel)"
wrapper-class="w-full"
/>
<ButtonStyled color="brand">
<button
class="!w-full"
:disabled="!emailModel || !passwordModel"
@click="onContinueWithEmail()"
>
{{ formatMessage(messages.continueWithEmail) }} <RightArrowIcon />
</button>
</ButtonStyled>
<p v-if="!routeQuery.launcher" class="m-0 text-center">
<IntlFormatted :message-id="messages.legalDisclaimer">
<template #terms-link="{ children }">
<NuxtLink to="/legal/terms" class="text-link">
<component :is="() => children" />
</NuxtLink>
</template>
<template #privacy-policy-link="{ children }">
<NuxtLink to="/legal/privacy" class="text-link">
<component :is="() => children" />
</NuxtLink>
</template>
</IntlFormatted>
</p>
<div class="mx-auto flex flex-wrap items-center justify-start gap-2 text-center">
{{ formatMessage(messages.alreadyHaveAccountLabel) }}
<NuxtLink
class="mr-1 text-link"
:to="{
path: '/auth/sign-in',
query: routeQuery,
}"
>
{{ formatMessage(commonMessages.signInButton) }}
</NuxtLink>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import {
DiscordColorIcon,
GitHubColorIcon,
GitLabColorIcon,
GoogleColorIcon,
KeyIcon,
MailIcon,
MicrosoftColorIcon,
RightArrowIcon,
SteamColorIcon,
} from '@modrinth/assets'
import {
ButtonStyled,
commonMessages,
defineMessages,
IntlFormatted,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { useStorage } from '@vueuse/core'
import type { LocationQuery } from 'vue-router'
import { getAuthUrl, PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY } from '@/composables/auth.ts'
type AuthProvider = 'discord' | 'google' | 'github' | 'gitlab' | 'steam' | 'microsoft'
interface Props {
redirectTarget?: string
showOtherOptions?: boolean
routeQuery?: LocationQuery
onToggleOtherOptions?: () => void
onContinueWithEmail?: () => void
}
const {
redirectTarget = '',
showOtherOptions = false,
routeQuery = {},
onToggleOtherOptions = () => {},
onContinueWithEmail = () => {},
} = defineProps<Props>()
const emailModel = defineModel<string>('email', { default: '' })
const passwordModel = defineModel<string>('password', { default: '' })
const pendingSignInOAuthProvider = useStorage<AuthProvider | null>(
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
null,
)
const onOAuthProviderClick = (provider: AuthProvider) => {
pendingSignInOAuthProvider.value = provider
}
const { formatMessage } = useVIntl()
const messages = defineMessages({
signUpWithTitle: {
id: 'auth.sign-up.title.sign-up-with',
defaultMessage: 'Create an Account',
},
continueWithProvider: {
id: 'auth.continue-with-provider',
defaultMessage: 'Continue with {provider}',
},
legalDisclaimer: {
id: 'auth.sign-up.legal-dislaimer',
defaultMessage:
"By creating an account, you agree to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>.",
},
alreadyHaveAccountLabel: {
id: 'auth.sign-up.sign-in-option.title',
defaultMessage: 'Already have an account?',
},
continueWithEmail: {
id: 'auth.sign-up.continue-with-email',
defaultMessage: 'Continue with Email',
},
showFewerOptions: {
id: 'auth.sign-up.show-fewer-options',
defaultMessage: 'Show fewer options',
},
showOtherOptions: {
id: 'auth.sign-up.show-other-options',
defaultMessage: 'Show other options',
},
})
</script>
@@ -89,6 +89,8 @@ import {
} from '@modrinth/ui'
import { ref } from 'vue'
import { generateUrlSlug } from '~/utils/slugs'
import CreateLimitAlert from './CreateLimitAlert.vue'
const router = useNativeRouter()
@@ -148,7 +150,7 @@ async function createOrganization(): Promise<void> {
const value = {
name: name.value.trim(),
description: description.value.trim(),
slug: slug.value.trim().replace(/ +/g, ''),
slug: slug.value.trim(),
}
const result: any = await useBaseFetch('organization', {
@@ -183,12 +185,7 @@ function hide(): void {
function updateSlug(): void {
if (!manualSlug.value) {
slug.value = name.value
.trim()
.toLowerCase()
.replaceAll(' ', '-')
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, '')
.replaceAll(/--+/gm, '-')
slug.value = generateUrlSlug(name.value)
}
}
@@ -145,6 +145,8 @@ import {
} from '@modrinth/ui'
import { computed, defineAsyncComponent, h } from 'vue'
import { generateUrlSlug } from '~/utils/slugs'
import CreateLimitAlert from './CreateLimitAlert.vue'
type ProjectTypes = 'server' | 'project'
@@ -164,9 +166,7 @@ defineExpose({
show,
})
const auth = (await useAuth()) as Ref<{
user: { id: string; username: string; avatar_url: string } | null
}>
const auth = await useAuth()
const messages = defineMessages({
title: {
@@ -461,12 +461,7 @@ async function show(event?: MouseEvent, options?: ShowOptions) {
function updatedName() {
if (!manualSlug.value) {
slug.value = name.value
.trim()
.toLowerCase()
.replaceAll(' ', '-')
.replaceAll(/[^a-zA-Z0-9!@$()`.+,_"-]/g, '')
.replaceAll(/--+/gm, '-')
slug.value = generateUrlSlug(name.value)
}
}
</script>
@@ -120,7 +120,7 @@ import { computed, onMounted, ref, watch } from 'vue'
import RevenueInputField from '@/components/ui/dashboard/RevenueInputField.vue'
import WithdrawFeeBreakdown from '@/components/ui/dashboard/WithdrawFeeBreakdown.vue'
import { getAuthUrl, removeAuthProvider, useAuth } from '@/composables/auth.js'
import { getAuthUrl, removeAuthProvider, useAuth } from '@/composables/auth.ts'
import { useWithdrawContext } from '@/providers/creator-withdraw.ts'
const { withdrawData, maxWithdrawAmount, availableMethods, calculateFees, saveStateToStorage } =
@@ -193,7 +193,6 @@ async function saveVenmoHandle() {
},
})
// @ts-expect-error auth.js is not typed
await useAuth(auth.value.token)
initialVenmoHandle.value = venmoHandle.value.trim()
@@ -358,8 +358,8 @@ import { computed, onMounted, ref, watch } from 'vue'
import RevenueInputField from '@/components/ui/dashboard/RevenueInputField.vue'
import WithdrawFeeBreakdown from '@/components/ui/dashboard/WithdrawFeeBreakdown.vue'
import { useAuth } from '@/composables/auth.js'
import { useWithdrawContext } from '@/providers/creator-withdraw.ts'
import { useAuth } from '~/composables/auth.ts'
const debug = useDebugLogger('TremendousDetailsStage')
const { withdrawData, maxWithdrawAmount, availableMethods, paymentOptions, calculateFees } =
@@ -24,7 +24,7 @@
</div>
<div class="input-group">
<ButtonStyled circular>
<button :class="!collapsed && '[&>svg]:rotate-180'" @click="$emit('toggleCollapsed')">
<button :class="{ '[&>svg]:rotate-180': !collapsed }" @click="$emit('toggleCollapsed')">
<DropdownIcon class="duration-250 transition-transform ease-in-out" />
</button>
</ButtonStyled>
@@ -190,7 +190,6 @@ import {
LinkIcon,
} from '@modrinth/assets'
import { type ExtendedReport, reportQuickReplies } from '@modrinth/moderation'
import { type OverflowMenuOption, useFormatDateTime } from '@modrinth/ui'
import {
Avatar,
ButtonStyled,
@@ -198,6 +197,8 @@ import {
getProjectTypeIcon,
injectNotificationManager,
OverflowMenu,
type OverflowMenuOption,
useFormatDateTime,
useRelativeTime,
} from '@modrinth/ui'
import { formatProjectType } from '@modrinth/utils'
@@ -11,11 +11,15 @@ import {
CopyIcon,
DownloadIcon,
EllipsisVerticalIcon,
EyeOffIcon,
LinkIcon,
LoaderCircleIcon,
ScaleIcon,
ShieldCheckIcon,
SpinnerIcon,
TimerIcon,
TriangleAlertIcon,
XIcon,
} from '@modrinth/assets'
import { type TechReviewContext, techReviewQuickReplies } from '@modrinth/moderation'
import {
@@ -23,15 +27,17 @@ import {
ButtonStyled,
Collapsible,
CollapsibleRegion,
commonMessages,
getProjectTypeIcon,
injectModrinthClient,
injectNotificationManager,
NavTabs,
OverflowMenu,
type OverflowMenuOption,
useFormatBytes,
useFormatDateTime,
useVIntl,
} from '@modrinth/ui'
import { NavTabs } from '@modrinth/ui'
import {
capitalizeString,
formatProjectType,
@@ -47,6 +53,7 @@ import ThreadView from '~/components/ui/thread/ThreadView.vue'
const auth = await useAuth()
const featureFlags = useFeatureFlags()
const { formatMessage } = useVIntl()
const formatDateTimeUtc = useFormatDateTime({
year: 'numeric',
@@ -96,6 +103,16 @@ const emit = defineEmits<{
showMaliciousSummary: [unsafeFiles: UnsafeFile[]]
}>()
const projectStatus = ref<Labrinth.Projects.v2.ProjectStatus>(props.item.project.status)
const isProjectApproved = computed(() => {
return (
projectStatus.value === 'approved' ||
projectStatus.value === 'archived' ||
projectStatus.value === 'unlisted' ||
projectStatus.value === 'private'
)
})
const quickActions = computed<OverflowMenuOption[]>(() => {
const actions: OverflowMenuOption[] = []
@@ -141,6 +158,53 @@ const quickActions = computed<OverflowMenuOption[]>(() => {
return actions
})
const isLoadingStatusAction = ref(false)
const projectStatusActions = computed<OverflowMenuOption[]>(() => [
{
id: 'approve',
color: 'green',
action: () => setStatus('approved'),
hoverFilled: true,
disabled: isProjectApproved.value || isLoadingStatusAction.value,
},
{
id: 'withhold',
color: 'orange',
action: () => setStatus('withheld'),
hoverFilled: true,
disabled: projectStatus.value === 'withheld' || isLoadingStatusAction.value,
},
{
id: 'send-to-review',
action: () => setStatus('processing'),
hoverFilled: true,
disabled: projectStatus.value === 'processing' || isLoadingStatusAction.value,
},
{
id: 'reject',
color: 'red',
action: () => setStatus('rejected'),
hoverFilled: true,
disabled: projectStatus.value === 'rejected' || isLoadingStatusAction.value,
},
])
async function setStatus(status: Labrinth.Projects.v2.ProjectStatus) {
isLoadingStatusAction.value = true
try {
await client.labrinth.projects_v2.edit(props.item.project.id, { status })
projectStatus.value = status
} catch (err) {
addNotification({
title: formatMessage(commonMessages.errorNotificationTitle),
text: (err as any)?.data?.description ? (err as any).data.description : String(err),
type: 'error',
})
}
isLoadingStatusAction.value = false
}
type Tab = 'Thread' | 'Files' | 'File'
const tabs: readonly ('Thread' | 'Files')[] = ['Thread', 'Files']
const currentTab = ref<Tab>('Thread')
@@ -347,13 +411,6 @@ const severityColor = computed(() => {
}
})
const isProjectApproved = computed(() => {
const status = props.item.project.status
return (
status === 'approved' || status === 'archived' || status === 'unlisted' || status === 'private'
)
})
const formattedDate = computed(() => {
const dates = props.item.reports.map((r) => new Date(r.created))
const earliest = new Date(Math.min(...dates.map((d) => d.getTime())))
@@ -1117,6 +1174,37 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
<BugIcon /> Fail
</button>
</ButtonStyled>
<ButtonStyled color="standard">
<OverflowMenu
class="btn-dropdown-animation"
:disabled="isLoadingStatusAction"
:options="projectStatusActions"
>
<SpinnerIcon
v-if="isLoadingStatusAction"
class="animate-spin"
aria-hidden="true"
/>
<ScaleIcon v-else aria-hidden="true" />
Set Status
<template #approve>
<CheckIcon aria-hidden="true" />
Approve
</template>
<template #withhold>
<EyeOffIcon aria-hidden="true" />
Withhold
</template>
<template #send-to-review>
<ScaleIcon aria-hidden="true" />
Send to review
</template>
<template #reject>
<XIcon aria-hidden="true" />
Reject
</template>
</OverflowMenu>
</ButtonStyled>
<ButtonStyled v-if="featureFlags.developerMode" type="outlined">
<button @click="emit('showMaliciousSummary', unsafeFiles)">Debug Summary</button>
</ButtonStyled>
@@ -87,7 +87,7 @@ const currentProjectId = computed(() => projectV3.value?.id)
const { selectedProjectId, selectedVersionId } = injectServerCompatibilityContext()
const { labrinth } = injectModrinthClient()
const { addNotification } = injectNotificationManager()
const auth = (await useAuth()) as { user?: { id: string } }
const auth = await useAuth()
interface VersionInfo {
id: string
@@ -59,7 +59,7 @@
</ButtonStyled>
<ButtonStyled v-if="visibleQuickReplies.length > 0">
<OverflowMenu :options="visibleQuickReplies">
Quick Reply
Quick reply
<ChevronDownIcon />
</OverflowMenu>
</ButtonStyled>
-179
View File
@@ -1,179 +0,0 @@
function normalizeAuthToken(value) {
if (typeof value === 'string') {
return value
}
return ''
}
export const useAuth = async (oldToken = null) => {
const auth = useState('auth', () => ({
user: null,
token: '',
headers: {},
}))
if (!auth.value.user || oldToken) {
auth.value = await initAuth(oldToken)
}
return auth
}
export const initAuth = async (oldToken = null) => {
const auth = {
user: null,
token: '',
}
if (oldToken === 'none') {
return auth
}
const route = useRoute()
const config = useRuntimeConfig()
const authCookie = useCookie('auth-token', {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
secure: config.public.cookieSecure,
httpOnly: false,
path: '/',
})
if (oldToken) {
const normalized = normalizeAuthToken(oldToken)
if (normalized) {
authCookie.value = normalized
}
}
const oauthCode = normalizeAuthToken(route.query.code)
if (oauthCode && !route.fullPath.includes('new_account=true')) {
authCookie.value = oauthCode
}
if (route.fullPath.includes('new_account=true') && route.path !== '/auth/welcome') {
const redirect = route.path.startsWith('/auth/') ? null : route.fullPath
await navigateTo(
`/auth/welcome?authToken=${oauthCode}${
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''
}`,
)
}
const tokenStr = normalizeAuthToken(authCookie.value)
if (authCookie.value != null && tokenStr === '') {
authCookie.value = null
} else if (tokenStr) {
auth.token = tokenStr
if (!auth.token.startsWith('mra_')) {
return auth
}
try {
auth.user = await useBaseFetch(
'user',
{
apiVersion: 3,
headers: {
Authorization: auth.token,
},
},
true,
)
} catch {
/* empty */
}
}
if (!auth.user && auth.token && typeof auth.token === 'string') {
try {
const session = await useBaseFetch(
'session/refresh',
{
method: 'POST',
headers: {
Authorization: auth.token,
},
},
true,
)
auth.token = normalizeAuthToken(session.session)
if (auth.token) {
authCookie.value = auth.token
auth.user = await useBaseFetch(
'user',
{
apiVersion: 3,
headers: {
Authorization: auth.token,
},
},
true,
)
} else {
authCookie.value = null
auth.token = ''
}
} catch {
authCookie.value = null
}
}
return auth
}
export const getSignInRedirectPath = (route) => {
const fullPath = route.fullPath
if (fullPath === '/auth' || fullPath.startsWith('/auth/')) {
return '/dashboard'
}
return fullPath
}
export const getSignInRouteObj = (route, redirectOverride) => ({
path: '/auth/sign-in',
query: {
redirect: redirectOverride ?? getSignInRedirectPath(route),
},
})
export const getAuthUrl = (provider, redirect = '/dashboard') => {
const config = useRuntimeConfig()
const route = useNativeRoute()
const fullURL = route.query.launcher
? getLauncherRedirectUrl(route)
: `${config.public.siteUrl}/auth/sign-in?redirect=${encodeURIComponent(redirect)}`
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`
}
export const removeAuthProvider = async (provider) => {
startLoading()
const auth = await useAuth()
await useBaseFetch('auth/provider', {
method: 'DELETE',
body: {
provider,
},
})
await useAuth(auth.value.token)
stopLoading()
}
export const getLauncherRedirectUrl = (route) => {
const usesLocalhostRedirectionScheme =
['4', '6'].includes(route.query.ipver) && Number(route.query.port) < 65536
return usesLocalhostRedirectionScheme
? `http://${route.query.ipver === '4' ? '127.0.0.1' : '[::1]'}:${route.query.port}`
: `https://launcher-files.modrinth.com`
}
+241
View File
@@ -0,0 +1,241 @@
import type { Labrinth } from '@modrinth/api-client'
import { useStorage } from '@vueuse/core'
import type { LocationQueryValue, RouteLocationNormalizedLoaded } from 'vue-router'
import type { CookieOptions } from '#app'
type AuthState = {
user: Labrinth.Users.v2.User | null
token: string
}
type QueryValue = LocationQueryValue | LocationQueryValue[] | undefined
type FullPathRoute = Pick<RouteLocationNormalizedLoaded, 'fullPath'>
type LauncherRoute = Pick<RouteLocationNormalizedLoaded, 'query'>
export const LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY = 'auth-last-sign-in-oauth-provider'
export const PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY = 'auth-pending-sign-in-oauth-provider'
const AUTH_COOKIE_OPTIONS = {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
httpOnly: false,
path: '/',
} satisfies CookieOptions<string | null>
const normalizeAuthToken = (value: unknown) => {
if (typeof value === 'string') {
return value
}
return ''
}
const getQueryString = (value: QueryValue) => {
if (Array.isArray(value)) {
return value[0] ?? null
}
return value ?? null
}
export const useAuth = async (oldToken: string | null | undefined = null) => {
const auth = useState<AuthState>('auth', () => ({
user: null,
token: '',
}))
if (!auth.value.user || oldToken) {
auth.value = await initAuth(oldToken)
}
return auth
}
export const initAuth = async (oldToken: string | null | undefined = null) => {
const auth: AuthState = {
user: null,
token: '',
}
if (oldToken === 'none') {
return auth
}
const route = useRoute()
const config = useRuntimeConfig()
const authCookie = useCookie<string | null>('auth-token', {
...AUTH_COOKIE_OPTIONS,
secure: config.public.cookieSecure,
})
if (oldToken) {
const normalized = normalizeAuthToken(oldToken)
if (normalized) {
authCookie.value = normalized
}
}
const oauthCode = normalizeAuthToken(route.query.code)
if (oauthCode && !route.fullPath.includes('new_account=true')) {
authCookie.value = oauthCode
}
if (route.fullPath.includes('new_account=true') && route.path !== '/auth/welcome') {
const redirect = route.path.startsWith('/auth/') ? null : route.fullPath
await navigateTo(
`/auth/welcome?authToken=${oauthCode}${
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''
}`,
)
}
const tokenStr = normalizeAuthToken(authCookie.value)
if (authCookie.value != null && tokenStr === '') {
authCookie.value = null
} else if (tokenStr) {
auth.token = tokenStr
if (!auth.token.startsWith('mra_')) {
return auth
}
try {
auth.user = (await useBaseFetch(
'user',
{
apiVersion: 3,
headers: {
Authorization: auth.token,
},
},
true,
)) as Labrinth.Users.v2.User
} catch {
/* empty */
}
}
if (!auth.user && auth.token) {
try {
const session = (await useBaseFetch(
'session/refresh',
{
method: 'POST',
headers: {
Authorization: auth.token,
},
},
true,
)) as { session: unknown }
auth.token = normalizeAuthToken(session.session)
if (auth.token) {
authCookie.value = auth.token
auth.user = (await useBaseFetch(
'user',
{
apiVersion: 3,
headers: {
Authorization: auth.token,
},
},
true,
)) as Labrinth.Users.v2.User
} else {
authCookie.value = null
auth.token = ''
}
} catch {
authCookie.value = null
}
}
return auth
}
export const getSignInRedirectPath = (route: FullPathRoute) => {
const fullPath = route.fullPath
if (fullPath === '/auth' || fullPath.startsWith('/auth/')) {
return '/dashboard'
}
return fullPath
}
export const getSignInRouteObj = (route: FullPathRoute, redirectOverride?: string | null) => ({
path: '/auth/sign-in',
query: {
redirect: redirectOverride ?? getSignInRedirectPath(route),
},
})
export const getAuthUrl = (provider: string, redirect = '/dashboard') => {
const config = useRuntimeConfig()
const route = useNativeRoute()
const launcher = getQueryString(route.query.launcher)
const fullURL = launcher
? (() => {
const callbackUrl = new URL('/auth/sign-in', config.public.siteUrl)
callbackUrl.searchParams.set('launcher', launcher)
const ipver = getQueryString(route.query.ipver)
const port = getQueryString(route.query.port)
if (ipver) {
callbackUrl.searchParams.set('ipver', ipver)
}
if (port) {
callbackUrl.searchParams.set('port', port)
}
return callbackUrl.toString()
})()
: `${config.public.siteUrl}/auth/sign-in?redirect=${encodeURIComponent(redirect)}`
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`
}
export const promotePendingSignInOAuthProvider = () => {
if (!import.meta.client) return
const pending = useStorage<string | null>(
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
null,
undefined,
{ initOnMounted: true },
)
if (!pending.value) return
const last = useStorage<string | null>(LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY, null, undefined, {
initOnMounted: true,
})
last.value = pending.value
pending.value = null
}
export const removeAuthProvider = async (provider: string) => {
startLoading()
const auth = await useAuth()
await useBaseFetch('auth/provider', {
method: 'DELETE',
body: {
provider,
},
})
await useAuth(auth.value.token)
stopLoading()
}
export const getLauncherRedirectUrl = (route: LauncherRoute) => {
const ipver = getQueryString(route.query.ipver)
const port = Number(getQueryString(route.query.port))
const usesLocalhostRedirectionScheme = ['4', '6'].includes(ipver ?? '') && port < 65536
return usesLocalhostRedirectionScheme
? `http://${ipver === '4' ? '127.0.0.1' : '[::1]'}:${port}`
: 'https://launcher-files.modrinth.com'
}
@@ -48,7 +48,6 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
useV1ContentTabAPI: true,
labrinthApiCanary: false,
dismissedExternalProjectsInfo: false,
modpackPermissionsPage: false,
showAllBanners: false,
alwaysIgnoreErrorBanner: false,
showViewProdRouteBanner: false,
@@ -56,6 +55,8 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
showModeratorPrivateMessageHighlight: true,
archonApiStaging: false,
showHostingAccessInstanceAuditLog: false,
versionDevInfoCollapsed: true,
alwaysShowVersionDevInfo: false,
} as const)
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
@@ -8,4 +8,11 @@ export const versionQueryOptions = {
queryFn: () => client.labrinth.versions_v3.getVersion(versionId),
staleTime: STALE_TIME,
}),
fromProject: (projectId: string, versionIdOrNumber: string, client: AbstractModrinthClient) => ({
queryKey: ['project', projectId, 'version', 'v3', versionIdOrNumber] as const,
queryFn: () =>
client.labrinth.versions_v3.getVersionFromIdOrNumber(projectId, versionIdOrNumber),
staleTime: STALE_TIME,
}),
}
@@ -132,23 +132,39 @@ export function createLoaderParsers(
),
}
},
// Fabric
// Fabric (or Babric for mc version beta 1.7.3)
'fabric.mod.json': (file: string): InferredVersionInfo => {
const metadata = JSON.parse(file) as any
const detectedGameVersions = metadata.depends
const mcDependency = metadata.depends?.minecraft
const mcDependencies = Array.isArray(mcDependency) ? mcDependency : [mcDependency]
let detectedGameVersions = metadata.depends
? getGameVersionsMatchingSemverRange(metadata.depends.minecraft, simplifiedGameVersions)
: []
const loaders: string[] = []
// Detect Beta 1.7.3 -> Babric
const hasBabricVersion = mcDependencies.some(
(version: string | undefined) => version?.includes('1.0.0-beta.7.3'), // this is fabric's normalized mc version format
)
// Detect 1.3-1.13 -> legacy-fabric
const hasLegacyVersions = detectedGameVersions.some((version) => {
const match = version.match(/^1\.(\d+)/)
return match && parseInt(match[1]) >= 3 && parseInt(match[1]) <= 13
})
if (hasLegacyVersions) loaders.push('legacy-fabric')
else loaders.push('fabric')
if (hasBabricVersion) {
loaders.push('babric')
detectedGameVersions = gameVersions
.filter((version) => version.version === 'b1.7.3')
.map((version) => version.version)
} else if (hasLegacyVersions) {
loaders.push('legacy-fabric')
} else {
loaders.push('fabric')
}
return {
name: `${project.title} ${metadata.version}`,
@@ -50,15 +50,15 @@ export function getGameVersionsMatchingMavenRange(
ranges.push(range)
}
const LESS_THAN_EQUAL = /^\(,(.*)]$/
const LESS_THAN = /^\(,(.*)\)$/
const EQUAL = /^\[(.*)]$/
const GREATER_THAN_EQUAL = /^\[(.*),\)$/
const GREATER_THAN = /^\((.*),\)$/
const BETWEEN = /^\((.*),(.*)\)$/
const BETWEEN_EQUAL = /^\[(.*),(.*)]$/
const BETWEEN_LESS_THAN_EQUAL = /^\((.*),(.*)]$/
const BETWEEN_GREATER_THAN_EQUAL = /^\[(.*),(.*)\)$/
const LESS_THAN_EQUAL = /^\(,([^,]*)]$/
const LESS_THAN = /^\(,([^,]*)\)$/
const EQUAL = /^\[([^,]*)]$/
const GREATER_THAN_EQUAL = /^\[([^,]*),\)$/
const GREATER_THAN = /^\(([^,]*),\)$/
const BETWEEN = /^\(([^,]*),([^,]*)\)$/
const BETWEEN_EQUAL = /^\[([^,]*),([^,]*)]$/
const BETWEEN_LESS_THAN_EQUAL = /^\(([^,]*),([^,]*)]$/
const BETWEEN_GREATER_THAN_EQUAL = /^\[([^,]*),([^,]*)\)$/
const semverRanges = []
+95
View File
@@ -0,0 +1,95 @@
function ensurePasskeySupported() {
const supported =
typeof window !== 'undefined' &&
typeof window.PublicKeyCredential !== 'undefined' &&
typeof navigator !== 'undefined' &&
!!navigator.credentials
if (!supported) {
throw new Error('Passkeys are not supported by this browser.')
}
}
function base64urlToBuffer(base64url: string) {
return Uint8Array.from(atob(base64url.replace(/-/g, '+').replace(/_/g, '/')), (char) =>
char.charCodeAt(0),
)
}
function bufferToBase64url(buffer: ArrayBuffer) {
const bytes = new Uint8Array(buffer)
let str = ''
for (let i = 0; i < bytes.length; i++) {
str += String.fromCharCode(bytes[i])
}
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}
/**
* Creates a passkey credential using the browser's WebAuthn API.
*
* @param options The public key options for creating the passkey credential, provided by the server.
*/
export async function createPasskeyCredential(options: any) {
ensurePasskeySupported()
const publicKey = {
...options,
challenge: base64urlToBuffer(options.challenge),
user: {
...options.user,
id: base64urlToBuffer(options.user.id),
},
excludeCredentials: options.excludeCredentials?.map((cred: any) => ({
...cred,
id: base64urlToBuffer(cred.id),
})),
}
const credential = (await navigator.credentials.create({ publicKey })) as PublicKeyCredential
const response = credential.response as AuthenticatorAttestationResponse
return {
id: credential.id,
rawId: bufferToBase64url(credential.rawId),
type: credential.type,
response: {
clientDataJSON: bufferToBase64url(response.clientDataJSON),
attestationObject: bufferToBase64url(response.attestationObject),
},
extensions: credential.getClientExtensionResults(),
}
}
/**
* Authenticates a user using a passkey credential.
*
* @param options The public key options for authenticating the passkey credential, provided by the server.
*/
export async function getPasskeyCredential(options: any) {
ensurePasskeySupported()
const publicKey = {
...options,
challenge: base64urlToBuffer(options.challenge),
allowCredentials: options.allowCredentials?.map((cred: any) => ({
...cred,
id: base64urlToBuffer(cred.id),
})),
}
const credential = (await navigator.credentials.get({ publicKey })) as PublicKeyCredential
const response = credential.response as AuthenticatorAssertionResponse
return {
id: credential.id,
rawId: bufferToBase64url(credential.rawId),
type: credential.type,
response: {
clientDataJSON: bufferToBase64url(response.clientDataJSON),
authenticatorData: bufferToBase64url(response.authenticatorData),
signature: bufferToBase64url(response.signature),
userHandle: response.userHandle ? bufferToBase64url(response.userHandle) : null,
},
extensions: credential.getClientExtensionResults(),
}
}
+1 -1
View File
@@ -799,7 +799,7 @@ import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.
import OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue'
import ProjectCreateModal from '~/components/ui/create/ProjectCreateModal.vue'
import ModrinthFooter from '~/components/ui/ModrinthFooter.vue'
import { getSignInRouteObj } from '~/composables/auth.js'
import { getSignInRouteObj } from '~/composables/auth.ts'
import { errors as generatedStateErrors } from '~/generated/state.json'
import { getProjectTypeMessage } from '~/utils/i18n-project-type.ts'
import { hasActiveMidas } from '~/utils/user-membership.ts'
+105 -33
View File
@@ -8,6 +8,108 @@
"admin.billing.error.not-found": {
"message": "المستخدم غير موجود"
},
"analytics.action.add": {
"message": "إضافة"
},
"analytics.action.cancel": {
"message": "إلغاء"
},
"analytics.action.refresh": {
"message": "تحديث"
},
"analytics.action.reset": {
"message": "إعادة ضبط"
},
"analytics.breakdown.country": {
"message": "الدولة"
},
"analytics.breakdown.download-reason": {
"message": "سبب التنزيل"
},
"analytics.breakdown.download-source": {
"message": "مصدر التنزيل"
},
"analytics.breakdown.game-version": {
"message": "إصدار اللُعبة"
},
"analytics.breakdown.project": {
"message": "المشروع"
},
"analytics.breakdown.project-status": {
"message": "حالة المشروع"
},
"analytics.breakdown.project-version": {
"message": "إصدار المشروع"
},
"analytics.chart.action.show-all": {
"message": "أظهار الكل"
},
"analytics.chart.axis.playtime-hours": {
"message": "{hours} ساعة"
},
"analytics.chart.controls.display": {
"message": "عرض"
},
"analytics.chart.tooltip.total": {
"message": "مجموع"
},
"analytics.downloads.suffix": {
"message": "التنزيلات"
},
"analytics.filter.game-version-type.all": {
"message": "الكل"
},
"analytics.group-by.week": {
"message": "الأسبوع"
},
"analytics.group-by.year": {
"message": "السنة"
},
"analytics.loading.fetching-results": {
"message": "جلب البيانات..."
},
"analytics.options.loading": {
"message": "تحميل..."
},
"analytics.project-event.project-approved": {
"message": "المشروع قُبل"
},
"analytics.project-event.project-private": {
"message": "وضع المشروع خاص"
},
"analytics.project-event.project-status-changed": {
"message": "تم تغيير حالة المشروع"
},
"analytics.project-event.version-released": {
"message": ""
},
"analytics.project-status.draft": {
"message": "مسودة"
},
"analytics.project-status.private": {
"message": "خاص"
},
"analytics.project-status.rejected": {
"message": "رُفض المشروع"
},
"analytics.project.your": {
"message": "مشاريعك"
},
"analytics.query.label.project": {
"message": "المشروع:"
},
"analytics.stat.downloads": {
"message": "التنزيلات"
},
"analytics.stat.playtime": {
"message": "وقت اللعب"
},
"analytics.stat.playtime-hours": {
"message": "{hours} ساعة"
},
"analytics.table.search.placeholder": {
"message": "بحث..."
},
"app-marketing.download.description": {
"message": "تطبيقنا لسطح المكتب متاح على جميع المنصات، اختر الإصدار الذي تريده."
},
@@ -263,39 +365,21 @@
"auth.sign-in.2fa.placeholder": {
"message": "أدخل الرمز..."
},
"auth.sign-in.additional-options": {
"message": "<forgot-password-link>هل نسيت كلمة المرور؟</forgot-password-link> • <create-account-link>إنشاء حساب</create-account-link>"
},
"auth.sign-in.sign-in-with": {
"message": "تسجيل الدخول باستخدام"
},
"auth.sign-in.title": {
"message": "تسجيل الدخول"
},
"auth.sign-in.use-password": {
"message": "أو استخدم كلمة المرور"
},
"auth.sign-up.action.create-account": {
"message": "إنشاء حساب"
},
"auth.sign-up.legal-dislaimer": {
"message": "بإنشائك حسابًا، فإنك توافق على <terms-link>شروط مودرنث</terms-link> و<privacy-policy-link>سياسة الخصوصية</privacy-policy-link> الخاصة بها."
},
"auth.sign-up.notification.password-mismatch.text": {
"message": "كلمتا المرور غير متطابقتين!"
},
"auth.sign-up.sign-in-option.title": {
"message": "هل لديك حساب بالفعل؟"
},
"auth.sign-up.subscribe.label": {
"message": "اشترك لتصلك التحديثات حول مودرنث"
},
"auth.sign-up.title": {
"message": "إنشاء حساب"
},
"auth.sign-up.title.create-account": {
"message": "أو أنشئ حسابًا بنفسك"
},
"auth.sign-up.title.sign-up-with": {
"message": "سجّل باستخدام"
},
@@ -329,21 +413,6 @@
"auth.verify-email.title": {
"message": "تحقّق من البريد الإلكتروني"
},
"auth.welcome.checkbox.subscribe": {
"message": "اشترك في التحديثات حول مودرنث"
},
"auth.welcome.description": {
"message": "أنت الآن جزء من مجتمع رائع من المبدعين والمستكشفين الذين يقومون بالفعل بإنشاء المودات المذهلة، تثبيتها، والبقاء على اطلاع بآخر التحديثات."
},
"auth.welcome.label.tos": {
"message": "بإنشائك حسابًا، فإنك توافق على <terms-link>شروط مودرنث</terms-link> و<privacy-policy-link>سياسة الخصوصية</privacy-policy-link>."
},
"auth.welcome.long-title": {
"message": "مرحبًا بك في مودرنث!"
},
"auth.welcome.title": {
"message": "مرحبًا"
},
"collection.button.edit-icon": {
"message": "تعديل الأيقونة"
},
@@ -407,6 +476,9 @@
"create-project-version.create-modal.stage.add-files.admonition": {
"message": "الملفات التكميلية تستخدم لدعم الموارد مثل كود المصدر، وليس للإصدارات أو الأنواع البديلة."
},
"create.collection.collection-info": {
"message": "سيتم إنشاء مجموعتك الجديدة كمجموعة عامة تحتوي على {count, plural,\n one {# مشروع}\n other {# مشاريع}\n}."
},
"create.collection.create-collection": {
"message": "إنشاء مجموعة"
},
+128 -35
View File
@@ -71,6 +71,87 @@
"analytics.chart.controls.annotations": {
"message": "Anotace"
},
"analytics.chart.controls.button": {
"message": "Ovládání"
},
"analytics.chart.events.project-title": {
"message": "<project>{projectName}</project>: {title}"
},
"analytics.chart.tooltip.hide-entry": {
"message": "Skrýt {name} v grafu"
},
"analytics.chart.tooltip.show-entry": {
"message": "Zobrazit {name} v grafu"
},
"analytics.chart.view.area": {
"message": "Oblast"
},
"analytics.download-reason.update": {
"message": "Aktualizace"
},
"analytics.downloads.suffix": {
"message": "stažení"
},
"analytics.empty.no-projects": {
"message": "Žádné dostupné projekty"
},
"analytics.empty.no-projects-for-analytics": {
"message": "Žádné dostupné projekty pro analýzu "
},
"analytics.empty.select-project": {
"message": "Vyber alespoň jeden projekt pro zobrazení dat"
},
"analytics.group-by.1h": {
"message": "1h"
},
"analytics.group-by.6h": {
"message": "6h"
},
"analytics.group-by.date": {
"message": "Datum"
},
"analytics.group-by.day": {
"message": "Den"
},
"analytics.group-by.month": {
"message": "Měsíc"
},
"analytics.group-by.week": {
"message": "Týden "
},
"analytics.group-by.year": {
"message": "Rok"
},
"analytics.options.loading": {
"message": "Načítání..."
},
"analytics.project-status.approved": {
"message": "Schválený"
},
"analytics.project-status.archived": {
"message": "Archivovaný "
},
"analytics.project-status.other": {
"message": "Jiné"
},
"analytics.project-status.private": {
"message": "Soukromý "
},
"analytics.project-status.unlisted": {
"message": "Neveřejný "
},
"analytics.project-status.withheld": {
"message": "Pozastavený"
},
"analytics.project.all": {
"message": "Všechny projekty"
},
"analytics.query.label.project": {
"message": "Projekt:"
},
"analytics.value.other": {
"message": "Jiné"
},
"app-marketing.download.description": {
"message": "Naše aplikace je dostupná pro všechny počítačové platformy, vyberte si požadovanou verzi."
},
@@ -326,39 +407,21 @@
"auth.sign-in.2fa.placeholder": {
"message": "Zadejte kód..."
},
"auth.sign-in.additional-options": {
"message": "<forgot-password-link>Zapomněli jste heslo?</forgot-password-link> • <create-account-link>Vytvořte si účet</create-account-link>"
},
"auth.sign-in.sign-in-with": {
"message": "Přihlášte se pomocí"
},
"auth.sign-in.title": {
"message": "Přihlásit se"
},
"auth.sign-in.use-password": {
"message": "Nebo použijte heslo"
},
"auth.sign-up.action.create-account": {
"message": "Vytvořte účet"
},
"auth.sign-up.legal-dislaimer": {
"message": "Vytvořením účtu souhlasíte s <terms-link>Podmínkami</terms-link> a <privacy-policy-link>Zásadami ochrany osobních údajů</privacy-policy-link> Modrinthu."
},
"auth.sign-up.notification.password-mismatch.text": {
"message": "Hesla se neshodují!"
},
"auth.sign-up.sign-in-option.title": {
"message": "Již máte účet?"
},
"auth.sign-up.subscribe.label": {
"message": "Přihlásit se k odběru informací o Modrinthu"
},
"auth.sign-up.title": {
"message": "Registrace"
},
"auth.sign-up.title.create-account": {
"message": "Nebo si vytvořte účet"
},
"auth.sign-up.title.sign-up-with": {
"message": "Registrujte se pomocí"
},
@@ -392,21 +455,6 @@
"auth.verify-email.title": {
"message": "Ověřit e-mail"
},
"auth.welcome.checkbox.subscribe": {
"message": "Přihlásit se k odběru informací o Modrinthu"
},
"auth.welcome.description": {
"message": "Nyní jste součástí úžasné komunity tvůrců a průzkumníků, kteří již vytvářejí, stahují a aktualizují úžasné módy."
},
"auth.welcome.label.tos": {
"message": "Vytvořením účtu souhlasíte s <terms-link>Podmínkami</terms-link> a <privacy-policy-link>Zásadami ochrany osobních údajů</privacy-policy-link> Modrinthu."
},
"auth.welcome.long-title": {
"message": "Vítejte v Modrinthu!"
},
"auth.welcome.title": {
"message": "Vítejte"
},
"collection.button.edit-icon": {
"message": "Upravit ikonu"
},
@@ -467,6 +515,12 @@
"conversation-thread.action.add-private-note": {
"message": "Přidat soukromou poznámku"
},
"conversation-thread.action.approve": {
"message": "Schválit"
},
"conversation-thread.action.approve-with-reply": {
"message": "Schválit s odpovědí"
},
"conversation-thread.action.close-thread": {
"message": "Uzavřít vlákno"
},
@@ -488,6 +542,12 @@
"conversation-thread.action.reply-to-thread": {
"message": "Odpovědět vláknu"
},
"conversation-thread.action.resubmit-for-review": {
"message": "Znovu odeslat ke kontrole"
},
"conversation-thread.action.resubmit-for-review-with-reply": {
"message": "Znovu odeslat ke kontrole s odpovědí "
},
"conversation-thread.action.send": {
"message": "Odeslat"
},
@@ -497,6 +557,12 @@
"conversation-thread.action.send-to-review-with-reply": {
"message": "Odeslat ke kontrole s odpovědí"
},
"conversation-thread.action.withhold": {
"message": "Pozastavit "
},
"conversation-thread.action.withhold-with-reply": {
"message": "Pozastavit s odpovědí"
},
"conversation-thread.reply-editor.placeholder.reply": {
"message": "Odpovědět vláknu..."
},
@@ -509,6 +575,9 @@
"create-project-version.create-modal.stage.add-files.admonition": {
"message": "Doplňkové soubory slouží jako podpůrné materiály (např. zdrojový kód), ne jako alternativní verze či varianty."
},
"create.collection.collection-info": {
"message": "Tvá nová kolekce bude vytvořena jako veřejná a bude obsahovat {count, plural, =0 {žádné projekty} one {# projekt} few {# projekty} other {# projektů}}."
},
"create.collection.create-collection": {
"message": "Vytvořit kolekci"
},
@@ -683,6 +752,9 @@
"dashboard.collections.sort.recently-updated": {
"message": "Nedávno aktualizováno"
},
"dashboard.creator-tax-form-modal.confirmation.download-button": {
"message": "Stáhnout {formType}"
},
"dashboard.creator-tax-form-modal.confirmation.title": {
"message": "Hotovo! 🎉"
},
@@ -717,7 +789,7 @@
"message": "Vyhladaj štát..."
},
"dashboard.creator-withdraw-modal.method-selection.region": {
"message": "Región"
"message": "Oblast"
},
"dashboard.creator-withdraw-modal.muralpay-details.account-owner": {
"message": "Majitel účtu"
@@ -737,6 +809,9 @@
"dashboard.creator-withdraw-modal.muralpay-details.document-number-tax-id-placeholder": {
"message": "Zadejte DIČ"
},
"dashboard.creator-withdraw-modal.muralpay-details.network": {
"message": "Síť"
},
"dashboard.creator-withdraw-modal.paypal-details.account": {
"message": "Účet"
},
@@ -756,7 +831,7 @@
"message": "Přihlásit přes PayPal"
},
"dashboard.creator-withdraw-modal.stage.tax-form": {
"message": ""
"message": "Daňový formulář"
},
"dashboard.creator-withdraw-modal.tremendous-details.enter-denomination-placeholder": {
"message": "Zadejte množství"
@@ -779,6 +854,9 @@
"dashboard.creator-withdraw-modal.withdraw-limit-used": {
"message": "Vyčerpali jste svůj limit pro výběr <b>{withdrawLimit}</b>. Pro vyšší výběr musíte vyplnit daňový formulář."
},
"dashboard.discord-roles.role.creator": {
"message": "Tvůrce"
},
"dashboard.organizations.button.create": {
"message": "Vytvořit organizaci"
},
@@ -788,12 +866,18 @@
"dashboard.organizations.error.fetch": {
"message": "Nepodařilo se načíst organizace"
},
"dashboard.organizations.title": {
"message": "Organizace"
},
"dashboard.overview.notifications.button.mark-all-as-read": {
"message": "Označit vše jako přečtené"
},
"dashboard.overview.notifications.button.view-history": {
"message": "Ukázat historii"
},
"dashboard.overview.notifications.empty.no-unread": {
"message": "Nemáš žádná nepřečtená oznámení."
},
"dashboard.overview.notifications.history.label": {
"message": "Historie"
},
@@ -806,6 +890,9 @@
"dashboard.projects.head-title": {
"message": "Projekty"
},
"dashboard.projects.links.button.edit": {
"message": "Upravit odkazy"
},
"dashboard.projects.links.discord-invite.label": {
"message": "Discord pozvánka"
},
@@ -824,6 +911,9 @@
"dashboard.projects.table.status": {
"message": "Stav"
},
"dashboard.report.title": {
"message": "Nahlásit {id}"
},
"dashboard.revenue.available-now": {
"message": "K dispozici nyní"
},
@@ -1268,6 +1358,9 @@
"profile.meta.description-with-bio": {
"message": "{bio} - Stáhněte si projekty od {username} na Modrinthu"
},
"profile.stats.projects-followers": {
"message": "{count, plural, one {<stat>{count}</stat> sledující projektu} few {<stat>{count}</stat> sledující projektu} other {<stat>{count}</stat> sledujících projektu}}"
},
"profile.user-id": {
"message": "ID uživatele: {id}"
},
@@ -263,39 +263,21 @@
"auth.sign-in.2fa.placeholder": {
"message": "Indtast kode..."
},
"auth.sign-in.additional-options": {
"message": "<forgot-password-link>Glemt adgangskode?</forgot-password-link> • <create-account-link>Opret en konto</create-account-link>"
},
"auth.sign-in.sign-in-with": {
"message": "Log ind med"
},
"auth.sign-in.title": {
"message": "Log ind"
},
"auth.sign-in.use-password": {
"message": "Eller brug en adgangskode"
},
"auth.sign-up.action.create-account": {
"message": "Opret konto"
},
"auth.sign-up.legal-dislaimer": {
"message": "Ved at oprette en konto, accepterer du Modrinth's <terms-link>Vilkår</terms-link> og <privacy-policy-link>Privatlivspolitik</privacy-policy-link>."
},
"auth.sign-up.notification.password-mismatch.text": {
"message": "Adgangskoderne er ikke ens!"
},
"auth.sign-up.sign-in-option.title": {
"message": "Har du allerede en konto?"
},
"auth.sign-up.subscribe.label": {
"message": "Abonner på opdateringer om Modrinth"
},
"auth.sign-up.title": {
"message": "Tilmeld"
},
"auth.sign-up.title.create-account": {
"message": "Eller opret en konto selv"
},
"auth.sign-up.title.sign-up-with": {
"message": "Tilmeld dig med"
},
@@ -329,21 +311,6 @@
"auth.verify-email.title": {
"message": "Bekræft E-mail"
},
"auth.welcome.checkbox.subscribe": {
"message": "Abonner på opdateringer om Modrinth"
},
"auth.welcome.description": {
"message": "Du er nu en del af det awesome fællesskab og skabere og udforskere der allerede bygger, downloader, og forbliver opdateret med fantastiske mods."
},
"auth.welcome.label.tos": {
"message": "Ved at oprette en konto, accepterer du Modrinth's <terms-link>Vilkår</terms-link> og <privacy-policy-link>Privatlivspolitik</privacy-policy-link>."
},
"auth.welcome.long-title": {
"message": "Velkommen til Modrinth!"
},
"auth.welcome.title": {
"message": "Velkommen"
},
"collection.button.edit-icon": {
"message": "Rediger ikon"
},

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