5 Commits

Author SHA1 Message Date
d83f839ba4 Merge pull request 'Remove unnecessary workflows' (#23) from beta into release
Reviewed-on: #23
2025-11-02 10:08:25 +03:00
c1d7d66ab0 Merge pull request 'Update README markdown language files' (#22) from beta into release
Reviewed-on: #22
2025-11-02 10:06:13 +03:00
78d2652b08 Merge pull request 'Merge beta into release' (#21) from beta into release
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Successful in 36m46s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
Reviewed-on: #21
2025-11-01 16:04:23 +03:00
e3065c2dfb Merge pull request 'beta' (#14) from beta into release
Reviewed-on: #14
2025-08-17 00:13:38 +03:00
f54f09becf Merge pull request 'beta' (#12) from beta into release
Reviewed-on: #12
2025-07-26 12:49:02 +03:00
540 changed files with 8987 additions and 74268 deletions

View File

@@ -1,6 +1,9 @@
[build]
rustflags = ["--cfg", "tokio_unstable"]
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
[target.'cfg(windows)']
rustflags = ["-C", "link-args=/STACK:16777220"]
rustflags = ["--cfg", "tokio_unstable", "-C", "link-args=/STACK:16777220"]
[target.x86_64-pc-windows-msvc]
linker = "rust-lld"

View File

@@ -9,7 +9,6 @@ Please follow these rules precisely:
1. Identify translatable strings
- Scan the <template> for all user-visible strings (inner text, alt attributes, placeholders, button labels, etc.). Do not extract dynamic expressions (like {{ user.name }}) or HTML tags. Only extract static human-readable text.
- There may be strings within the <script> block, e.g dropdown option labels, notifications etc.
2. Create message definitions

3
.gitignore vendored
View File

@@ -65,6 +65,3 @@ app-playground-data/*
.astro
.claude
# labrinth demo fixtures
apps/labrinth/fixtures/demo

View File

@@ -13,7 +13,7 @@
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[vue]": {
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"

View File

@@ -57,7 +57,3 @@ Use `docker exec labrinth-clickhouse clickhouse-client` to access the Clickhouse
### Postgres
Use `docker exec labrinth-postgres psql -U postgres` to access the PostgreSQL instance.
# Guidelines
- Do not create new non-source code files (e.g. Bash scripts, SQL scripts) unless explicitly prompted to.

1307
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,6 @@ actix-rt = "2.11.0"
actix-web = "4.11.0"
actix-web-prom = "0.10.0"
actix-ws = "0.3.0"
arc-swap = "1.7.1"
argon2 = { version = "0.5.3", features = ["std"] }
ariadne = { path = "packages/ariadne" }
async-compression = { version = "0.4.32", default-features = false }
@@ -55,6 +54,7 @@ clap = "4.5.48"
clickhouse = "0.14.0"
color-eyre = "0.6.5"
color-thief = "0.2.2"
console-subscriber = "0.4.1"
const_format = "0.2.34"
daedalus = { path = "packages/daedalus" }
dashmap = "6.1.0"
@@ -109,7 +109,6 @@ maxminddb = "0.26.0"
meilisearch-sdk = { version = "0.30.0", default-features = false }
modrinth-maxmind = { path = "packages/modrinth-maxmind" }
modrinth-util = { path = "packages/modrinth-util" }
muralpay = { path = "packages/muralpay" }
murmur2 = "0.1.0"
native-dialog = "0.9.2"
notify = { version = "8.2.0", default-features = false }
@@ -140,7 +139,6 @@ rust-s3 = { version = "0.37.0", default-features = false, features = [
] }
rustls = "0.23.32"
rusty-money = "0.4.1"
secrecy = "0.10.3"
sentry = { version = "0.45.0", default-features = false, features = [
"backtrace",
"contexts",
@@ -163,7 +161,6 @@ sha2 = "0.10.9"
shlex = "1.3.0"
spdx = "0.12.0"
sqlx = { version = "0.8.6", default-features = false }
strum = "0.27.2"
sysinfo = { version = "0.37.2", default-features = false }
tar = "0.4.44"
tauri = "2.8.5"
@@ -249,7 +246,6 @@ redundant_clone = "warn"
redundant_feature_names = "warn"
redundant_type_annotations = "warn"
todo = "warn"
too_many_arguments = "allow"
uninlined_format_args = "warn"
unnested_or_patterns = "warn"
wildcard_dependencies = "warn"

View File

@@ -9,7 +9,7 @@ extend-exclude = [
# contains licenses like `CC-BY-ND-4.0`
"packages/moderation/src/data/stages/license.ts",
# contains payment card IDs like `IY1VMST1MOXS` which are flagged
"apps/labrinth/src/queue/payouts/mod.rs",
"apps/labrinth/src/queue/payouts.rs",
]
[default.extend-words]

View File

@@ -13,13 +13,11 @@
"test": "vue-tsc --noEmit"
},
"dependencies": {
"@modrinth/api-client": "workspace:^",
"@sfirew/minecraft-motd-parser": "^1.1.6",
"@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
"@sentry/vue": "^8.27.0",
"@sfirew/minecraft-motd-parser": "^1.1.6",
"@tanstack/vue-query": "^5.90.7",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-http": "^2.5.0",
@@ -43,9 +41,9 @@
"vue-virtual-scroller": "v2.0.0-beta.8"
},
"devDependencies": {
"@modrinth/tooling-config": "workspace:*",
"@eslint/compat": "^1.1.1",
"@formatjs/cli": "^6.2.12",
"@modrinth/tooling-config": "workspace:*",
"@nuxt/eslint-config": "^0.5.6",
"@taijased/vue-render-tracker": "^1.0.7",
"@vitejs/plugin-vue": "^5.0.4",

View File

@@ -1,5 +1,4 @@
<script setup>
import { AuthFeature, TauriModrinthClient } from '@modrinth/api-client'
import {
ArrowBigUpDashIcon,
ChangeSkinIcon,
@@ -19,14 +18,12 @@ import {
RefreshCwIcon,
RestoreIcon,
RightArrowIcon,
ServerIcon,
SettingsIcon,
UserIcon,
WorldIcon,
XIcon,
} from '@modrinth/assets'
import {
Admonition,
Avatar,
Button,
ButtonStyled,
@@ -35,12 +32,9 @@ import {
NotificationPanel,
OverflowMenu,
ProgressSpinner,
provideModrinthClient,
provideNotificationManager,
useDebugLogger,
provideNotificationManager
} from '@modrinth/ui'
import { renderString } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { getVersion } from '@tauri-apps/api/app'
import { invoke } from '@tauri-apps/api/core'
import { getCurrentWindow } from '@tauri-apps/api/window'
@@ -69,8 +63,7 @@ import RunningAppBar from '@/components/ui/RunningAppBar.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { check_reachable } from '@/helpers/auth.js'
import { debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { get_user } from '@/helpers/cache.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { useFetch } from '@/helpers/fetch.js'
@@ -104,16 +97,6 @@ const notificationManager = new AppNotificationManager()
provideNotificationManager(notificationManager)
const { handleError, addNotification } = notificationManager
const tauriApiClient = new TauriModrinthClient({
userAgent: `modrinth/theseus/${getVersion()} (support@modrinth.com)`,
features: [
new AuthFeature({
token: async () => (await getCreds()).session,
}),
],
})
provideModrinthClient(tauriApiClient)
const news = ref([])
const availableSurvey = ref(false)
@@ -138,27 +121,6 @@ const criticalErrorMessage = ref()
const isMaximized = ref(false)
const authUnreachableDebug = useDebugLogger('AuthReachableChecker')
const authServerQuery = useQuery({
queryKey: ['authServerReachability'],
queryFn: async () => {
await check_reachable()
authUnreachableDebug('Auth servers are reachable')
return true
},
refetchInterval: 5 * 60 * 1000, // 5 minutes
retry: false,
refetchOnWindowFocus: false,
})
const authUnreachable = computed(() => {
if (authServerQuery.isError.value && !authServerQuery.isLoading.value) {
console.warn('Failed to reach auth servers', authServerQuery.error.value)
return true
}
return false
})
onMounted(async () => {
await useCheckDisableMouseover()
await getRemote(false) // [AR] Check for updates
@@ -194,15 +156,6 @@ const messages = defineMessages({
id: 'app.update.downloading-update',
defaultMessage: 'Downloading update ({percent}%)',
},
authUnreachableHeader: {
id: 'app.auth-servers.unreachable.header',
defaultMessage: 'Cannot reach authentication servers',
},
authUnreachableBody: {
id: 'app.auth-servers.unreachable.body',
defaultMessage:
'Minecraft authentication servers may be down right now. Check your internet connection and try again later.',
},
})
async function setupApp() {
@@ -362,11 +315,7 @@ const handleClose = async () => {
const router = useRouter()
router.afterEach((to, from, failure) => {
trackEvent('PageView', {
path: to.path,
fromPath: from.path,
failed: failure,
})
trackEvent('PageView', { path: to.path, fromPath: from.path, failed: failure })
})
const route = useRoute()
@@ -390,7 +339,7 @@ async function fetchCredentials() {
if (creds && creds.user_id) {
creds.user = await get_user(creds.user_id).catch(handleError)
}
credentials.value = creds ?? null
credentials.value = creds
}
async function signIn() {
@@ -621,11 +570,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
<template>
<SplashScreen v-if="!stateFailed" ref="splashScreen" data-tauri-drag-region />
<div id="teleports"></div>
<div
v-if="stateInitialized"
class="app-grid-layout experimental-styles-within relative"
:class="{ 'disable-advanced-rendering': !themeStore.advancedRendering }"
>
<div v-if="stateInitialized" class="app-grid-layout experimental-styles-within relative">
<Suspense>
<AppSettingsModal ref="settingsModal" />
</Suspense>
@@ -644,13 +589,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
<NavButton v-if="themeStore.featureFlags.worlds_tab" v-tooltip.right="'Worlds'" to="/worlds">
<WorldIcon />
</NavButton>
<NavButton
v-if="themeStore.featureFlags.servers_in_app"
v-tooltip.right="'Servers'"
to="/servers/manage"
>
<ServerIcon />
</NavButton>
<NavButton
v-tooltip.right="'Discover content'"
to="/browse/modpack"
@@ -674,7 +612,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
>
<LibraryIcon />
</NavButton>
<div class="h-px w-6 mx-auto my-2 bg-surface-5"></div>
<div class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
<suspense>
<QuickInstanceSwitcher />
</suspense>
@@ -835,10 +773,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
<div
v-if="stateInitialized"
class="app-contents experimental-styles-within"
:class="{
'sidebar-enabled': sidebarVisible,
'disable-advanced-rendering': !themeStore.advancedRendering,
}"
:class="{ 'sidebar-enabled': sidebarVisible }"
>
<div class="app-viewport flex-grow router-view">
<transition name="popup-survey">
@@ -886,25 +821,16 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
width: 'calc(100% - var(--right-bar-width))',
}"
></div>
<Admonition
<div
v-if="criticalErrorMessage"
type="critical"
:header="criticalErrorMessage.header"
class="m-6 mb-0"
class="m-6 mb-0 flex flex-col border-red bg-bg-red rounded-2xl border-2 border-solid p-4 gap-1 font-semibold text-contrast"
>
<h1 class="m-0 text-lg font-extrabold">{{ criticalErrorMessage.header }}</h1>
<div
class="markdown-body text-primary"
v-html="renderString(criticalErrorMessage.body ?? '')"
></div>
</Admonition>
<Admonition
v-if="authUnreachable"
type="warning"
:header="formatMessage(messages.authUnreachableHeader)"
class="m-6 mb-0"
>
{{ formatMessage(messages.authUnreachableBody) }}
</Admonition>
</div>
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Suspense @pending="loading.startLoading()" @resolve="loading.stopLoading()">
@@ -1106,7 +1032,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
.app-sidebar::before {
content: '';
box-shadow: -15px 0 15px -15px rgba(0, 0, 0, 0.1) inset;
box-shadow: -15px 0 15px -15px rgba(0, 0, 0, 0.2) inset;
top: 0;
bottom: 0;
left: -2rem;
@@ -1131,10 +1057,9 @@ provideAppUpdateDownloadProgress(appUpdateDownload) // [AR Note] If delete this
right: calc(-1 * var(--left-bar-width));
bottom: calc(-1 * var(--left-bar-width));
border-radius: var(--radius-xl);
box-shadow: 1px 1px 15px rgba(0, 0, 0, 0.1) inset;
border-color: var(--surface-5);
border-width: 1px;
border-style: solid;
box-shadow:
1px 1px 15px rgba(0, 0, 0, 0.2) inset,
inset 1px 1px 1px rgba(255, 255, 255, 0.23);
pointer-events: none;
}

View File

@@ -12,7 +12,6 @@ import {
} from '@modrinth/assets'
import { Button, DropdownSelect, injectNotificationManager } from '@modrinth/ui'
import { formatCategoryHeader } from '@modrinth/utils'
import { useStorage } from '@vueuse/core'
import dayjs from 'dayjs'
import { computed, ref } from 'vue'
@@ -122,50 +121,40 @@ const handleOptionsClick = async (args) => {
}
}
const state = useStorage(
`${props.label}-grid-display-state`,
{
group: 'Group',
sortBy: 'Name',
},
localStorage,
{ mergeDefaults: true },
)
const search = ref('')
const group = ref('Group')
const sortBy = ref('Name')
const filteredResults = computed(() => {
const { group = 'Group', sortBy = 'Name' } = state.value
const instances = props.instances.filter((instance) => {
return instance.name.toLowerCase().includes(search.value.toLowerCase())
})
if (sortBy === 'Name') {
if (sortBy.value === 'Name') {
instances.sort((a, b) => {
return a.name.localeCompare(b.name)
})
}
if (sortBy === 'Game version') {
if (sortBy.value === 'Game version') {
instances.sort((a, b) => {
return a.game_version.localeCompare(b.game_version, undefined, { numeric: true })
})
}
if (sortBy === 'Last played') {
if (sortBy.value === 'Last played') {
instances.sort((a, b) => {
return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0))
})
}
if (sortBy === 'Date created') {
if (sortBy.value === 'Date created') {
instances.sort((a, b) => {
return dayjs(b.date_created).diff(dayjs(a.date_created))
})
}
if (sortBy === 'Date modified') {
if (sortBy.value === 'Date modified') {
instances.sort((a, b) => {
return dayjs(b.date_modified).diff(dayjs(a.date_modified))
})
@@ -173,7 +162,7 @@ const filteredResults = computed(() => {
const instanceMap = new Map()
if (group === 'Loader') {
if (group.value === 'Loader') {
instances.forEach((instance) => {
const loader = formatCategoryHeader(instance.loader)
if (!instanceMap.has(loader)) {
@@ -182,7 +171,7 @@ const filteredResults = computed(() => {
instanceMap.get(loader).push(instance)
})
} else if (group === 'Game version') {
} else if (group.value === 'Game version') {
instances.forEach((instance) => {
if (!instanceMap.has(instance.game_version)) {
instanceMap.set(instance.game_version, [])
@@ -190,7 +179,7 @@ const filteredResults = computed(() => {
instanceMap.get(instance.game_version).push(instance)
})
} else if (group === 'Group') {
} else if (group.value === 'Group') {
instances.forEach((instance) => {
if (instance.groups.length === 0) {
instance.groups.push('None')
@@ -210,7 +199,7 @@ const filteredResults = computed(() => {
// For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance
// ie: Category A should come before B, even if the first instance in B comes before the first instance in A
if (sortBy === 'Name') {
if (sortBy.value === 'Name') {
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
// None should always be first
if (a[0] === 'None' && b[0] !== 'None') {
@@ -228,7 +217,7 @@ const filteredResults = computed(() => {
}
// default sorting would do 1.20.4 < 1.8.9 because 2 < 8
// localeCompare with numeric=true puts 1.8.9 < 1.20.4 because 8 < 20
if (group === 'Game version') {
if (group.value === 'Game version') {
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
return a[0].localeCompare(b[0], undefined, { numeric: true })
})
@@ -252,7 +241,7 @@ const filteredResults = computed(() => {
</div>
<DropdownSelect
v-slot="{ selected }"
v-model="state.sortBy"
v-model="sortBy"
name="Sort Dropdown"
class="max-w-[16rem]"
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
@@ -263,7 +252,7 @@ const filteredResults = computed(() => {
</DropdownSelect>
<DropdownSelect
v-slot="{ selected }"
v-model="state.group"
v-model="group"
class="max-w-[16rem]"
name="Group Dropdown"
:options="['Group', 'Loader', 'Game version', 'None']"

View File

@@ -571,12 +571,12 @@ onUnmounted(() => {
z-index: 11;
gap: 0.5rem;
padding: 1rem;
border: 1px solid var(--color-divider);
border: 1px solid var(--color-button-bg);
width: max-content;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
max-height: calc(100vh - 300px);
max-height: 98vh;
overflow-y: auto;
&::-webkit-scrollbar-track {
@@ -673,7 +673,7 @@ onUnmounted(() => {
text-align: left;
&.expanded {
border: 1px solid var(--color-divider);
border: 1px solid var(--color-button-bg);
padding: 1rem;
}
}

View File

@@ -119,7 +119,7 @@ onBeforeUnmount(() => {
background-color: var(--color-raised-bg);
border-radius: var(--radius-md);
box-shadow: var(--shadow-floating);
border: 1px solid var(--color-divider);
border: 1px solid var(--color-button-bg);
margin: 0;
position: fixed;
z-index: 1000000;
@@ -163,7 +163,7 @@ onBeforeUnmount(() => {
}
.divider {
border: 1px solid var(--color-divider);
border: 1px solid var(--color-button-bg);
margin: var(--gap-sm);
pointer-events: none;
}

View File

@@ -34,7 +34,7 @@
</div>
<div class="input-row">
<p class="input-label">Game version</p>
<div class="flex gap-4 items-center">
<div class="versions">
<multiselect
v-model="game_version"
class="selector"
@@ -45,7 +45,7 @@
open-direction="top"
:show-labels="false"
/>
<Checkbox v-model="showSnapshots" class="shrink-0" label="Show all versions" />
<Checkbox v-model="showSnapshots" class="filter-checkbox" label="Show all versions" />
</div>
</div>
<div v-if="loader !== 'vanilla'" class="input-row">
@@ -563,6 +563,12 @@ const next = async () => {
font-style: italic;
}
.versions {
display: flex;
flex-direction: row;
gap: 1rem;
}
:deep(button.checkbox) {
border: none;
}

View File

@@ -69,7 +69,7 @@ onUnmounted(() => {
<SpinnerIcon class="animate-spin w-4 h-4" />
</div>
</NavButton>
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-divider"></div>
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
</template>
<style scoped lang="scss"></style>

View File

@@ -293,7 +293,7 @@ onBeforeUnmount(() => {
align-items: center;
gap: 0.5rem;
border-radius: var(--radius-md);
border: 1px solid var(--color-divider);
border: 1px solid var(--color-button-bg);
padding: var(--gap-sm) var(--gap-lg);
}
@@ -356,7 +356,7 @@ onBeforeUnmount(() => {
gap: 1rem;
overflow: auto;
transition: all 0.2s ease-in-out;
border: 1px solid var(--color-divider);
border: 1px solid var(--color-button-bg);
&.hidden {
transform: translateY(-100%);
@@ -454,7 +454,7 @@ onBeforeUnmount(() => {
flex-direction: column;
overflow: auto;
transition: all 0.2s ease-in-out;
border: 1px solid var(--color-divider);
border: 1px solid var(--color-button-bg);
padding: var(--gap-md);
&.hidden {

View File

@@ -15,8 +15,8 @@ import {
ButtonStyled,
Checkbox,
Chips,
Combobox,
injectNotificationManager,
TeleportDropdownMenu,
} from '@modrinth/ui'
import {
formatCategory,
@@ -164,21 +164,6 @@ const selectableGameVersionNumbers = computed(() => {
.map((x) => x.version)
})
const gameVersionOptions = computed(() =>
(selectableGameVersionNumbers.value ?? []).map((v) => ({ value: v, label: v })),
)
const loaderVersionOptions = computed(() =>
(selectableLoaderVersions.value ?? []).map((opt, index) => ({ value: index, label: opt.id })),
)
const loaderVersionLabel = computed(() => {
const idx = loaderVersionIndex.value
return idx >= 0 && selectableLoaderVersions.value
? selectableLoaderVersions.value[idx]?.id
: 'Select version'
})
const selectableLoaderVersions: ComputedRef<ManifestLoaderVersion[] | undefined> = computed(() => {
if (gameVersion.value) {
if (loader.value === 'fabric') {
@@ -702,11 +687,11 @@ async function handleInitAuthLibPatching(ismojang: boolean) {
{{ formatMessage(messages.gameVersion) }}
</h2>
<div class="flex flex-wrap mt-2 gap-2">
<Combobox
<TeleportDropdownMenu
v-if="selectableGameVersionNumbers !== undefined"
v-model="gameVersion"
:options="gameVersionOptions"
:display-value="gameVersion || formatMessage(messages.unknownVersion)"
:options="selectableGameVersionNumbers"
name="Game Version Dropdown"
/>
<Checkbox
v-if="hasSnapshots"
@@ -718,13 +703,14 @@ async function handleInitAuthLibPatching(ismojang: boolean) {
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.loaderVersion, { loader: formatCategory(loader) }) }}
</h2>
<Combobox
<TeleportDropdownMenu
v-if="selectableLoaderVersions"
v-model="loaderVersionIndex"
:options="loaderVersionOptions"
:display-value="loaderVersionLabel"
:model-value="selectableLoaderVersions[loaderVersionIndex]"
:options="selectableLoaderVersions"
:display-name="(option: ManifestLoaderVersion) => option?.id"
name="Version selector"
class="mt-2"
@change="(value) => (loaderVersionIndex = value.index)"
/>
<div v-else class="mt-2 text-brand-red flex gap-2 items-center">
<IssuesIcon />

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Combobox, ThemeSelector, Toggle } from '@modrinth/ui'
import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui'
import { ref, watch } from 'vue'
import { get, set } from '@/helpers/settings.ts'
@@ -50,7 +50,7 @@ watch(
:model-value="themeStore.advancedRendering"
@update:model-value="
(e) => {
themeStore.advancedRendering = !!e
themeStore.advancedRendering = e
settings.advanced_rendering = themeStore.advancedRendering
}
"
@@ -86,13 +86,12 @@ watch(
<h2 class="m-0 text-lg font-extrabold text-contrast">Default landing page</h2>
<p class="m-0 mt-1">Change the page to which the launcher opens on.</p>
</div>
<Combobox
<TeleportDropdownMenu
id="opening-page"
v-model="settings.default_page"
name="Opening page dropdown"
class="w-40"
:options="['Home', 'Library'].map((v) => ({ value: v, label: v }))"
:display-value="settings.default_page ?? 'Select an option'"
:options="['Home', 'Library']"
/>
</div>
@@ -123,7 +122,7 @@ watch(
:model-value="settings.toggle_sidebar"
@update:model-value="
(e) => {
settings.toggle_sidebar = !!e
settings.toggle_sidebar = e
themeStore.toggleSidebar = settings.toggle_sidebar
}
"

View File

@@ -130,7 +130,7 @@ onUnmounted(() => {
/>
</template>
<div
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised card-shadow rounded-xl smart-clickable:highlight-on-hover"
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised rounded-xl smart-clickable:highlight-on-hover"
>
<Avatar
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"

View File

@@ -181,7 +181,7 @@ const messages = defineMessages({
/>
</template>
<div
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised card-shadow smart-clickable:highlight-on-hover rounded-xl"
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised smart-clickable:highlight-on-hover rounded-xl"
:class="{
'world-item-highlighted': highlighted,
}"

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Combobox } from '@modrinth/ui'
import { TeleportDropdownMenu } from '@modrinth/ui'
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import type { ServerPackStatus } from '@/helpers/worlds.ts'
@@ -74,19 +74,12 @@ defineExpose({ resourcePackOptions })
{{ formatMessage(messages.resourcePack) }}
</h2>
<div>
<Combobox
<TeleportDropdownMenu
v-model="resourcePack"
:options="
resourcePackOptions.map((o) => ({
value: o,
label: formatMessage(resourcePackOptionMessages[o]),
}))
"
:options="resourcePackOptions"
name="Server resource pack"
:display-value="
resourcePack
? formatMessage(resourcePackOptionMessages[resourcePack])
: 'Select an option'
:display-name="
(option: ServerPackStatus) => formatMessage(resourcePackOptionMessages[option])
"
/>
</div>

View File

@@ -35,14 +35,6 @@ export async function elyby_auth_authenticate(login, password, clientToken) {
})
}
/**
* Check if the authentication servers are reachable, throwing an exception if
* not reachable.
*/
export async function check_reachable() {
await invoke('plugin:auth|check_reachable')
}
/**
* Authenticate a user with Hydra - part 1.
* This begins the authentication flow quasi-synchronously.

View File

@@ -18,7 +18,7 @@ import { install_to_existing_profile } from '@/helpers/pack.js'
- icon is a path to an image file, which will be copied into the profile directory
*/
export async function create(name, gameVersion, modloader, loaderVersion, icon, skipInstall) {
export async function create(name, gameVersion, modloader, loaderVersion, iconPath, skipInstall) {
//Trim string name to avoid "Unable to find directory"
name = name.trim()
return await invoke('plugin:profile-create|profile_create', {
@@ -26,7 +26,7 @@ export async function create(name, gameVersion, modloader, loaderVersion, icon,
gameVersion,
modloader,
loaderVersion,
icon,
iconPath,
skipInstall,
})
}

View File

@@ -1,9 +1,6 @@
{
"app.settings.developer-mode-enabled": {
"message": "تم تفعيل وضع المطوّر."
},
"app.settings.downloading": {
"message": "جار تنزيل الإصدار {version}"
"message": "وضع المطوّر مُفعَّل."
},
"app.settings.tabs.appearance": {
"message": "المظهر"
@@ -12,7 +9,7 @@
"message": "خيارات النسخة الافتراضية"
},
"app.settings.tabs.feature-flags": {
"message": "أعلام الميزات"
"message": "إعدادات المميزات"
},
"app.settings.tabs.java-installations": {
"message": "تثبيتات جافا"
@@ -23,111 +20,6 @@
"app.settings.tabs.resource-management": {
"message": "إدارة الموارد"
},
"app.update-toast.body": {
"message": "تطبيق Modrinth الإصدار {version} جاهز للتثبيت!\nأعد التحميل لتحديث التطبيق الآن، أو سيتم التحديث تلقائيًا عند إغلاق تطبيق Modrinth."
},
"app.update-toast.body.download-complete": {
"message": "تطبيق Modrinth الإصدار {version} جاهز للتثبيت!\nأعد التحميل لتحديث التطبيق الآن، أو سيتم التحديث تلقائيًا عند إغلاق تطبيق Modrinth."
},
"app.update-toast.body.metered": {
"message": "تطبيق Modrinth الإصدار {version} متاح الآن!\nنظرًا لأنك تستخدم شبكة محدودة البيانات، لم نقم بتنزيل التحديث تلقائيًا.\n"
},
"app.update-toast.changelog": {
"message": "سجلّ التغييرات"
},
"app.update-toast.download": {
"message": "تنزيل ({size})"
},
"app.update-toast.downloading": {
"message": "جار التنزيل..."
},
"app.update-toast.reload": {
"message": "إعادة تحميل"
},
"app.update-toast.title": {
"message": "تحديث متاح"
},
"app.update-toast.title.download-complete": {
"message": "اكتمل التنزيل"
},
"app.update.complete-toast.text": {
"message": "انقر هنا لعرض سجلّ التغييرات."
},
"app.update.complete-toast.title": {
"message": "تم تثبيت الإصدار {version} بنجاح!"
},
"app.update.download-update": {
"message": "تنزيل التحديث"
},
"app.update.downloading-update": {
"message": "جار تنزيل التحديث ({percent}٪)"
},
"app.update.reload-to-update": {
"message": "أعد التحميل لتثبيت التحديث"
},
"friends.action.add-friend": {
"message": "إضافة صديق"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {طلب صداقة} other {طلبات صداقة}}"
},
"friends.add-friend.submit": {
"message": "إرسال طلب صداقة"
},
"friends.add-friend.title": {
"message": "جار إضافة صديق"
},
"friends.add-friend.username.description": {
"message": "قد يختلف عن اسم المستخدم الخاص بهم في Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "أدخل اسم مستخدم Modrinth..."
},
"friends.add-friend.username.title": {
"message": "ما اسم مستخدم صديقك في Modrinth؟"
},
"friends.add-friends-to-share": {
"message": "<link>أضف أصدقاء</link> لمعرفة ما الذي يلعبونه!"
},
"friends.friend.cancel-request": {
"message": "إلغاء الطلب"
},
"friends.friend.remove-friend": {
"message": "إزالة صديق"
},
"friends.friend.request-sent": {
"message": "إرسال طلب الصداقة"
},
"friends.friend.view-profile": {
"message": "عرض الملف الشخصي"
},
"friends.heading": {
"message": "أصدقاء"
},
"friends.heading.active": {
"message": "نشط"
},
"friends.heading.offline": {
"message": "غير متصل"
},
"friends.heading.online": {
"message": "متصل"
},
"friends.heading.pending": {
"message": "قيد الانتظار"
},
"friends.no-friends-match": {
"message": "لا يوجد أصدقاء يطابقون ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "ابحث عن الأصدقاء..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>سجّل الدخول إلى حساب Modrinth</link> لإضافة الأصدقاء ومعرفة ما الذي يلعبونه!"
},
"instance.add-server.add-and-play": {
"message": "إضافة واللعب"
},

View File

@@ -1,8 +1,5 @@
{
"app.settings.developer-mode-enabled": {
"message": ""
},
"app.settings.downloading": {
"message": ""
}
}

View File

@@ -12,7 +12,7 @@
"message": "Mga kapilian sa sukaranan nga pananglitan"
},
"app.settings.tabs.feature-flags": {
"message": "Bandera sa mga panagway"
"message": "Bandera sa mga bahin"
},
"app.settings.tabs.java-installations": {
"message": "Mga pagtaod sa Java"
@@ -21,118 +21,34 @@
"message": "Pribasiya"
},
"app.settings.tabs.resource-management": {
"message": "Pagdumala sa kahinguhaan"
},
"app.update-toast.body": {
"message": "Andam na mataud ang Modrinth App v{version}! Pagkarga kausab aron mapasibo, o kinaugalingon pagtak-op sa Modrinth App."
"message": "Pagdumala sa kabtangan"
},
"app.update-toast.body.download-complete": {
"message": "Nahuman ang pagkarganug sa Modrinth App v{version}. Pagkarga kausab aron mapasibo, o kinaugalingon pagtak-op sa Modrinth App."
"message": "Nahuman ang pagkarganug sa Modrinth App v{version}. Pagkarga kausab aron matuman ang kabag-ohan, o unya nalang sa pagsara sa Modrinth App."
},
"app.update-toast.body.metered": {
"message": "Magamit na karon ang Modrinth App! Wala namo karganugi daan kay inihap man ang imong pum-ot."
},
"app.update-toast.changelog": {
"message": "Talaan sa Kausaban"
},
"app.update-toast.download": {
"message": "Karganugi ({size})"
},
"app.update-toast.downloading": {
"message": "Gakarganug..."
},
"app.update-toast.reload": {
"message": "Kargaha pag-usab"
},
"app.update-toast.title": {
"message": "Naay bag-o nga pagpasibo"
},
"app.update-toast.title.download-complete": {
"message": "Nahuman ang pagkarganug"
},
"app.update.complete-toast.text": {
"message": "Panuplok diri aron malantaw ang talaan sa kausaban."
},
"app.update.complete-toast.title": {
"message": "Malampusong nataud ang bersiyon nga {version}!"
},
"app.update.download-update": {
"message": "Karganugi ang kausaban"
},
"app.update.downloading-update": {
"message": "Gakarganug sa pagpasibo ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Andam mataud ang pagpasibo"
},
"friends.action.add-friend": {
"message": "Pagdugang og higala"
},
"friends.action.view-friend-requests": {
"message": "{count} ka hangyo sa pakighigala"
},
"friends.add-friend.submit": {
"message": "Pagpadala og hangyo sa pakighigala"
},
"friends.add-friend.title": {
"message": "Pagdugang og higala"
},
"friends.add-friend.username.description": {
"message": "Mahimong galahi sa ngalan nila sa Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Ibutang ang ngalan sa tiggamit sa Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Unsa man ang ngalan sa imong higala sa Modrinth?"
},
"friends.add-friends-to-share": {
"message": "<link>Pagdugang og mga higala</link> aron makit-an ang ilang ginadula!"
},
"friends.friend.cancel-request": {
"message": "Bawia ang hangyo"
},
"friends.friend.remove-friend": {
"message": "Tangtangi ang higala"
},
"friends.friend.request-sent": {
"message": "Gipadala na ang hangyo sa pakighigala"
},
"friends.friend.view-profile": {
"message": "Tan-awa ang propayl"
},
"friends.heading": {
"message": "Mga higala"
},
"friends.heading.active": {
"message": "Malihokon"
},
"friends.heading.offline": {
"message": "Sira"
},
"friends.heading.online": {
"message": "Buka"
},
"friends.heading.pending": {
"message": "Gahulat"
},
"friends.no-friends-match": {
"message": "Walay higala nga motukma sa \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Mangita sa mga higala..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Pag-sign-in sa Modrinth account</link> aron makadugang og mga higala ug mahibal-an ang ginadula nila!"
"message": "Gakarganug sa kausaban ({percent}%)"
},
"instance.add-server.add-and-play": {
"message": "Idugang ug dulaa"
},
"instance.add-server.add-server": {
"message": "Idugang ang magsisilbi"
"message": "Idugang ang tigtagad"
},
"instance.add-server.resource-pack.disabled": {
"message": "Dili motugot"
@@ -144,13 +60,13 @@
"message": "Magpatugot"
},
"instance.add-server.title": {
"message": "Pagdugang og magsisilbi"
"message": "Pagdugang og tigtagad"
},
"instance.edit-server.title": {
"message": "Usba ang magsisilbi"
"message": "Usba ang tigtagad"
},
"instance.edit-world.hide-from-home": {
"message": "Ayaw ipakita sa Puluy-anang panid"
"message": "Ayaw ipakita sa panid sa Balay"
},
"instance.edit-world.name": {
"message": "Ngalan"
@@ -159,55 +75,37 @@
"message": "Minecraft nga Kalibutan"
},
"instance.edit-world.reset-icon": {
"message": "Pag-usab sa amoy"
"message": "Walaa ang amoy"
},
"instance.edit-world.title": {
"message": "Usba ang kalibutan"
},
"instance.filter.disabled": {
"message": "Di-paganhong mga proyekto"
"message": "Di-gagana nga mga proyekto"
},
"instance.filter.updates-available": {
"message": "Naay bag-ong mga kausaban"
},
"instance.server-modal.address": {
"message": "Padad-anan"
},
"instance.server-modal.name": {
"message": "Ngalan"
},
"instance.server-modal.placeholder-name": {
"message": "Minecraft nga Magsisilbi"
"message": "Minecraft nga Tigtagad"
},
"instance.server-modal.resource-pack": {
"message": "Putos sa kahinguhaan"
},
"instance.settings.tabs.general": {
"message": "Tinanan"
"message": "Putos sa kabtangan"
},
"instance.settings.tabs.general.delete": {
"message": "Panas-i kining pananglitan"
"message": "Tangtangi ang pananglitan"
},
"instance.settings.tabs.general.delete.button": {
"message": "Panas-i kining pananglitan"
"message": "Tangtangi ang pananglitan"
},
"instance.settings.tabs.general.delete.description": {
"message": "Malungtarong mopanas ang pananglitan sa imong himan, apil na ang imong mga kalibutan, paghan-ay, ug tanang gitaod nga sulod. Pag-amping, dili na mabawi kung gipanas na nimo ang pananglitan."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Gapanas..."
},
"instance.settings.tabs.general.duplicate-button": {
"message": "Paghulad"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Dili makahulad samtang nga gataud."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "Paghulad sa pananglitan"
"message": "Malungtarong matangtang ang pananglitan sa imong himan, apil na ang imong mga kalibutan, paghan-ay, ug tanang gitaod nga sulod. Pag-amping, dili na mabawi kung tangtangon na nimo ang pananglitan."
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "Buhatan og awat kining pananglitan, apil na ang imong mga kalibutan, paghan-ay, kausaban, ug uban pa."
"message": "Buhatan og kopya kining pananglitan, apil na ang imong mga kalibutan, paghan-ay, kausaban, ug uban pa."
},
"instance.settings.tabs.general.edit-icon": {
"message": "Usba ang amoy"
@@ -215,259 +113,19 @@
"instance.settings.tabs.general.edit-icon.remove": {
"message": "Tangtangi ang amoy"
},
"instance.settings.tabs.general.edit-icon.replace": {
"message": "Pulihan ang amoy"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "Pamili og amoy"
},
"instance.settings.tabs.general.library-groups": {
"message": "Mga pundok sa librarya"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "Pagbuhat og bag-o nga pundok"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "Gitugotan sa mga pundok sa librarya nga imong mahan-ay ang imong mga pananglitan sa nagkalain-lain nga bahin sa imong librarya."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "Ibutang ang ngalan sa pundok"
},
"instance.settings.tabs.general.name": {
"message": "Ngalan"
},
"instance.settings.tabs.hooks": {
"message": "Mga kaw-it sa paglansad"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "Mga pinatuyo nga kaw-it sa paglansad"
},
"instance.settings.tabs.hooks.description": {
"message": "Gitugotan sa mga kaw-it ang mga eksperto nga mga tiggamit nga makapadagan og mga sistema nga sugo ayha ug paghuman malansad ang dula."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Human-matak-op"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "Ipadagan paghuman matak-op ang dula."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Ibutang ang human-matak-op nga sugo..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Ayha-malansad"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Ipadagan ayha malansad ang pananglitan."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "Ibutang ang ayha-malansad nga sugo..."
},
"instance.settings.tabs.hooks.title": {
"message": "Mga kaw-it sa paglansad sa dula"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Pamutos"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "Pamutos nga sugo sa paglansad sa Minecraft."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Ibutang ang pamutos nga sugo..."
},
"instance.settings.tabs.installation": {
"message": "Pagtaud"
},
"instance.settings.tabs.installation.change-version.already-installed.modded": {
"message": "Nataud naman ang {platform} {version} alang sa Minecraft {game_version}"
},
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
"message": "Nataud naman ang Banilya nga {game_version}"
},
"instance.settings.tabs.installation.change-version.button": {
"message": "Pulihan og bersiyon"
},
"instance.settings.tabs.installation.change-version.button.install": {
"message": "Itaud"
},
"instance.settings.tabs.installation.change-version.button.installing": {
"message": "Gataud"
},
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
"message": "Gapangita og mga bersiyon sa mga putos sa kausaban"
},
"instance.settings.tabs.installation.change-version.in-progress": {
"message": "Gataud sa bag-o nga bersiyon"
},
"instance.settings.tabs.installation.currently-installed": {
"message": "Pagkakarong taud"
},
"instance.settings.tabs.installation.debug-information": {
"message": "Kasayoran sa pagputli:"
},
"instance.settings.tabs.installation.game-version": {
"message": "Bersiyon sa dula"
},
"instance.settings.tabs.installation.install": {
"message": "Itaud"
},
"instance.settings.tabs.installation.install.in-progress": {
"message": "Nagtaud karon"
},
"instance.settings.tabs.installation.loader-version": {
"message": "Bersiyon sa {loader}"
},
"instance.settings.tabs.installation.minecraft-version": {
"message": "Minecraft {version}"
},
"instance.settings.tabs.installation.no-loader-versions": {
"message": "Dili magamit ang {loader} sa Minecraft {version}. Sulayi ang ubang tigkarga sa kausaban."
},
"instance.settings.tabs.installation.platform": {
"message": "Pantawan"
},
"instance.settings.tabs.installation.reinstall.button": {
"message": "Itaud pag-usab ang putos sa kausaban"
},
"instance.settings.tabs.installation.reinstall.button.reinstalling": {
"message": "Nagtaud pag-usab sa putos sa kusaban"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "Mahimo nga mobalik sa sinugdan ang tanang gitaod o giusab nga sulod sa unsay ihatag sa putos sa kausaban, tangtangon ang mga kausaban o sulod nga imong gidugang apil na ang lintunganay nga putos sa kausaban. Mahimo nga maayo ang mga tuhaw nga batasan kon naay pagbag-o sa pananglitan, apan kon gasalig na ang imong kalibutan sa dinugang nga sulod, mahimo nga madaut ani ang daan nga mga kalibutan."
},
"instance.settings.tabs.installation.reinstall.description": {
"message": "Ibalik ang mga sulod sa pananglitan sa unang kahimtang, tangtangon ang mga kausaban o sulod nga imong gidugang apil na ang lintunganay nga putos sa kausaban."
},
"instance.settings.tabs.installation.reinstall.title": {
"message": "Itaud pag-usab ang putos sa kausaban"
},
"instance.settings.tabs.installation.repair.button": {
"message": "Ayohon"
},
"instance.settings.tabs.installation.repair.button.repairing": {
"message": "Gaayo"
},
"instance.settings.tabs.installation.repair.confirm.description": {
"message": "Sa pag-ayo, mataud pagbalik ang mga sinaligan sa Minecraft ug mangita og mga kadunot. Mahimo nga masulbad niini ang mga isyu kun dili malunsad ang dula tungod sa mga kasaypan matud sa tiglansad, apan dili ni masulbad ang mga isyu o pagdusmog matud sa mga gitaud nga kausaban."
},
"instance.settings.tabs.installation.repair.confirm.title": {
"message": "Ayohon ang pananglitan?"
},
"instance.settings.tabs.installation.repair.in-progress": {
"message": "Nag-ayo karon"
},
"instance.settings.tabs.installation.reset-selections": {
"message": "Sa kasamtang pag-usab "
},
"instance.settings.tabs.installation.show-all-versions": {
"message": "Ipakita ang tanang bersiyon"
},
"instance.settings.tabs.installation.tooltip.action.change-version": {
"message": "pulihan og bersiyon"
},
"instance.settings.tabs.installation.tooltip.action.install": {
"message": "itaud"
},
"instance.settings.tabs.installation.tooltip.action.reinstall": {
"message": "itaud pag-usab"
},
"instance.settings.tabs.installation.tooltip.action.repair": {
"message": "ayohon"
},
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
"message": "Dili maka-{action} samtang nga gataud"
},
"instance.settings.tabs.installation.tooltip.cannot-while-offline": {
"message": "Dili maka-{action} samtang binugto"
},
"instance.settings.tabs.installation.tooltip.cannot-while-repairing": {
"message": "Dili maka-{action} samtang nag-ayo"
},
"instance.settings.tabs.installation.unknown-version": {
"message": "(diinilang bersiyon)"
},
"instance.settings.tabs.installation.unlink.button": {
"message": "Pagbugto sa pananglitan"
},
"instance.settings.tabs.installation.unlink.confirm.description": {
"message": "Kun imong ipadayon, dili na nimo makatay pagbalik nga wala mohimo og bag-o nga pananglitan. Dili na ka makadawat og pagpasibo sa putos sa kausaban ug mahimo kining naandan nga."
},
"instance.settings.tabs.installation.unlink.description": {
"message": "Nakakatay kining pananglitan sa usa ka putos sa kausaban, pasabot ani nga dili mapasibo ang mga kausaban og dili nimo mausab ang tigkarga sa kausaban o ang bersiyon sa Minecraft. Kanunay nga mabugto kining pananglitan og putos sa kausabon kun tangtangon ang katay."
},
"instance.settings.tabs.installation.unlink.title": {
"message": "Pagbugto sa putos sa kausaban"
},
"instance.settings.tabs.java": {
"message": "Java at memorya"
},
"instance.settings.tabs.java.hooks": {
"message": "Mga kaw-it"
},
"instance.settings.tabs.java.java-arguments": {
"message": "Mga lantugi sa java"
},
"instance.settings.tabs.java.java-installation": {
"message": "Pagtaud sa Java"
},
"instance.settings.tabs.java.java-memory": {
"message": "Memoryang gigahin"
},
"instance.settings.tabs.window": {
"message": "Tamboanan"
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "Mga himutangan sa pinatuyo nga tamboanan"
},
"instance.settings.tabs.window.fullscreen": {
"message": "Punong-tabil"
},
"instance.settings.tabs.window.height": {
"message": "Gitas-on"
},
"instance.settings.tabs.window.height.enter": {
"message": "Ibutang ang gitas-on..."
},
"instance.settings.tabs.window.width": {
"message": "Gilapdon"
},
"instance.settings.tabs.window.width.enter": {
"message": "Ibutang ang gilapdon..."
"message": "Mga gusto sa pinatuyo nga tamboanan"
},
"instance.settings.title": {
"message": "Mga Himutangan"
},
"instance.worlds.a_minecraft_server": {
"message": "Usa ka Minecraft nga Magsisilbi"
},
"instance.worlds.copy_address": {
"message": "Awata ang padad-anan"
},
"instance.worlds.dont_show_on_home": {
"message": "Ayaw pakit-a sa Puloy-anan"
},
"instance.worlds.filter.available": {
"message": "Magamit"
},
"instance.worlds.hardcore": {
"message": "Mahanasnon nga paagi"
},
"instance.worlds.play_instance": {
"message": "Dulai ang pananglitan"
},
"instance.worlds.type.server": {
"message": "Magsisilbi"
},
"instance.worlds.type.singleplayer": {
"message": "Inusara nga dula"
"message": "Mga Gusto"
},
"instance.worlds.view_instance": {
"message": "Tan-awa ang pananglitan"
},
"instance.worlds.world_in_use": {
"message": "Gigamit ang kalibotan"
},
"search.filter.locked.instance.sync": {
"message": "Pagdungan sa pananglitan"
}
}

View File

@@ -2,9 +2,6 @@
"app.settings.developer-mode-enabled": {
"message": "Udvikler-tilstand aktiveret."
},
"app.settings.downloading": {
"message": "Downloader v{version}"
},
"app.settings.tabs.appearance": {
"message": "Udseende"
},
@@ -23,111 +20,6 @@
"app.settings.tabs.resource-management": {
"message": "Ressourcestyring"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} er allerede installeret! Genindlæs for at opdatere nu, eller automatisk når du lukker Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} er færdig med at download. Genindlæs for at opdatere nu, eller automatisk når du lukker Modrinth App."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} er nu tilgængelig! Siden du er på et begrænset netværk, vi downloadede den ikke automatisk."
},
"app.update-toast.changelog": {
"message": "Ændringslog"
},
"app.update-toast.download": {
"message": "Download ({size})"
},
"app.update-toast.downloading": {
"message": "Downloader..."
},
"app.update-toast.reload": {
"message": "Geninlæs"
},
"app.update-toast.title": {
"message": "Opdatering tilgængelig"
},
"app.update-toast.title.download-complete": {
"message": "Download færdiggjort"
},
"app.update.complete-toast.text": {
"message": "Klik her for at vise ændringslog."
},
"app.update.complete-toast.title": {
"message": "Version {version} var installeret med succes!"
},
"app.update.download-update": {
"message": "Download opdatering"
},
"app.update.downloading-update": {
"message": "Downloader opdatering ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Genindlæs for at installere opdatering"
},
"friends.action.add-friend": {
"message": "Tilføj en ven"
},
"friends.action.view-friend-requests": {
"message": "{count} venne{count, plural, one {anmodning} other {anmodninger}}"
},
"friends.add-friend.submit": {
"message": "Send en venneanmodning"
},
"friends.add-friend.title": {
"message": "Tilføjer en ven"
},
"friends.add-friend.username.description": {
"message": "Det er muligvis anderledes end deres Minecraft brugernavn!"
},
"friends.add-friend.username.placeholder": {
"message": "Indskriv Modrinth brugernavn..."
},
"friends.add-friend.username.title": {
"message": "Hvad er din vens Modrinth brugernavn?"
},
"friends.add-friends-to-share": {
"message": "<link>Tilføj venner</link> for at se hvad de spiller!"
},
"friends.friend.cancel-request": {
"message": "Annuller anmodning"
},
"friends.friend.remove-friend": {
"message": "Fjern ven"
},
"friends.friend.request-sent": {
"message": "Venneanmodning sendt"
},
"friends.friend.view-profile": {
"message": "Vis profil"
},
"friends.heading": {
"message": "Venner"
},
"friends.heading.active": {
"message": "Aktiv"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Afventer"
},
"friends.no-friends-match": {
"message": "Ingen venner som matcher \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Søg venner..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Log ind på en Modrinth konto</link> for at tilføje venner og se hvad de spiller!"
},
"instance.add-server.add-and-play": {
"message": "Tilføj og spil"
},

View File

@@ -65,69 +65,6 @@
"app.update.reload-to-update": {
"message": "Lade neu um Aktualisierung zu installieren"
},
"friends.action.add-friend": {
"message": "Freund hinzufügen"
},
"friends.action.view-friend-requests": {
"message": "{count} Freundschaftsanfrage{count, plural, one {} other {n}}"
},
"friends.add-friend.submit": {
"message": "Freundschaftsanfrage senden"
},
"friends.add-friend.title": {
"message": "Einen Freund hinzufügen"
},
"friends.add-friend.username.description": {
"message": "Es könnte anders als ihr Minecraft Nutzername sein!"
},
"friends.add-friend.username.placeholder": {
"message": "Modrinth Nutzernamen eingeben..."
},
"friends.add-friend.username.title": {
"message": "Was ist der Modrinth Nutzername deines Freundes?"
},
"friends.add-friends-to-share": {
"message": "<link>Füge Freunde hinzu</link> um zu sehen, was sie spielen!"
},
"friends.friend.cancel-request": {
"message": "Anfrage abbrechen"
},
"friends.friend.remove-friend": {
"message": "Freund entfernen"
},
"friends.friend.request-sent": {
"message": "Freundschaftsanfrage gesendet"
},
"friends.friend.view-profile": {
"message": "Profil anzeigen"
},
"friends.heading": {
"message": "Freunde"
},
"friends.heading.active": {
"message": "Aktiv"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Ausstehend"
},
"friends.no-friends-match": {
"message": "Keine Freunde, die mit \"{query}\" übereinstimmen"
},
"friends.search-friends-placeholder": {
"message": "Freunde suchen..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Logge dich in ein Modrinth Konto ein</link> um Freunde hinzuzufügen und zu sehen, was sie spielen!"
},
"instance.add-server.add-and-play": {
"message": "Ersteue u starte"
},

View File

@@ -15,7 +15,7 @@
"message": "Funktionsflaggen"
},
"app.settings.tabs.java-installations": {
"message": "Java-Installationen"
"message": "Java Installationen"
},
"app.settings.tabs.privacy": {
"message": "Datenschutz"
@@ -24,16 +24,16 @@
"message": "Ressourcenmanagement"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} ist bereit zur Installation! Neu laden, um jetzt zu aktualisieren, oder automatisch, wenn du die Modrinth App schließt."
"message": "Modrinth App v{version} ist bereit zum Installieren! Lade neu um es jetzt zu updaten, oder automatisch wenn du die Modrinth App schließt."
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} wurde heruntergeladen. Neu laden, um jetzt zu aktualisieren, oder automatisch, wenn du die Modrinth App schließt."
"message": "Modrinth App v{version} ist heruntergeladen. Lade neu um es jetzt zu updaten, oder wenn du die Modrinth App schließt."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} ist jetzt verfügbar! Da du ein getaktetes Netzwerk nutzt, haben wir den Download nicht automatisch gestartet."
"message": "Die Modrinth App v{version} ist jetzt verfügbar! Da Sie sich in einem getakteten Netzwerk befinden, haben wir es nicht automatisch heruntergeladen."
},
"app.update-toast.changelog": {
"message": "Änderungsverlauf"
"message": "Änderungen"
},
"app.update-toast.download": {
"message": "Herunterladen ({size})"
@@ -45,89 +45,26 @@
"message": "Neu laden"
},
"app.update-toast.title": {
"message": "Update verfügbar"
"message": "Aktualisierung verfügbar"
},
"app.update-toast.title.download-complete": {
"message": "Download abgeschlossen"
"message": "Herunterladen abgeschlossen"
},
"app.update.complete-toast.text": {
"message": "Hier klicken, um das Änderungsprotokoll anzuzeigen."
"message": "Drücke hier um die Änderungen zu sehen."
},
"app.update.complete-toast.title": {
"message": "Version {version} wurde erfolgreich installiert!"
},
"app.update.download-update": {
"message": "Update herunterladen"
"message": "Lade Update herunter"
},
"app.update.downloading-update": {
"message": "Update wird heruntergeladen ({percent}%)"
"message": "Lade Update Herunter ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Neu laden um Update zu installieren"
},
"friends.action.add-friend": {
"message": "Freund hinzufügen"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {Freundesanfrage} other {Freundesanfragen}}"
},
"friends.add-friend.submit": {
"message": "Freundesanfrage senden"
},
"friends.add-friend.title": {
"message": "Einen Freund hinzufügen"
},
"friends.add-friend.username.description": {
"message": "Er kann vom Minecraft-Nutzernamen abweichen!"
},
"friends.add-friend.username.placeholder": {
"message": "Modrinth-Benutzernamen eingeben..."
},
"friends.add-friend.username.title": {
"message": "Wie lautet der Modrinth-Benutzername deines Freundes?"
},
"friends.add-friends-to-share": {
"message": "<link>Freunde hinzufügen</link>, um zu sehen, was sie spielen!"
},
"friends.friend.cancel-request": {
"message": "Anfrage abbrechen"
},
"friends.friend.remove-friend": {
"message": "Freund entfernen"
},
"friends.friend.request-sent": {
"message": "Freundesanfrage gesendet"
},
"friends.friend.view-profile": {
"message": "Profil anzeigen"
},
"friends.heading": {
"message": "Freunde"
},
"friends.heading.active": {
"message": "Aktiv"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Ausstehend"
},
"friends.no-friends-match": {
"message": "Keine Freunde die \"{query}\" entsprechen"
},
"friends.search-friends-placeholder": {
"message": "Freunde durchsuchen..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Melde dich bei einem Modrinth-Konto an</link>, um Freunde hinzuzufügen und zu sehen, was sie gerade spielen!"
},
"instance.add-server.add-and-play": {
"message": "Hinzufügen und spielen"
},
@@ -156,7 +93,7 @@
"message": "Name"
},
"instance.edit-world.placeholder-name": {
"message": "Minecraft-Welt"
"message": "Minecraft Welt"
},
"instance.edit-world.reset-icon": {
"message": "Icon zurücksetzen"
@@ -177,7 +114,7 @@
"message": "Name"
},
"instance.server-modal.placeholder-name": {
"message": "Minecraft-Server"
"message": "Minecraft Server"
},
"instance.server-modal.resource-pack": {
"message": "Ressourcenpaket"
@@ -192,16 +129,16 @@
"message": "Instanz löschen"
},
"instance.settings.tabs.general.delete.description": {
"message": "Löscht eine Instanz dauerhaft von deinem Gerät, einschließlich deiner Welten, Einstellungen und aller installierten Inhalte. Sei vorsichtig, da eine gelöschte Instanz nicht wiederhergestellt werden kann."
"message": "Löscht eine Instanz dauerhaft von deinem Gerät, einschließlich deiner Welten, Einstellungen und aller installierten Inhalte. Sei vorsichtig, eine gelöschte Installation ist nicht wiederherstellbar."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Wird gelöscht..."
},
"instance.settings.tabs.general.duplicate-button": {
"message": "Kopieren"
"message": "Duplizieren"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Kann während der Installation nicht kopiert werden."
"message": "Kann während der Installation nicht dupliziert werden."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "Instanz duplizieren"
@@ -228,7 +165,7 @@
"message": "Neue Gruppe erstellen"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "Bibliotheksgruppen ermöglichen es dir, deine Instanzen in verschiedene Abschnitte deiner Bibliothek zu organisieren."
"message": "Gruppen ermöglichen dir, deine Instanzen in verschiedenen Abteilen deiner Bibliothek einzuteilen."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "Gruppenname eingeben"
@@ -246,25 +183,25 @@
"message": "Hooks ermöglichen es fortgeschrittenen Benutzern, bestimmte Systembefehle vor und nach dem Spielstart auszuführen."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Nach dem Beenden"
"message": "Nach dem Schließen des Spiels"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "Wird nach dem Beenden des Spiels ausgeführt."
"message": "Wird ausgeführt nach dem Beenden des Spiels."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Nach Spielbeendigung auszuführender Befehl eingeben..."
"message": "Ausgeführter Befehl nach dem Beenden des Spiels eingeben..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Vor dem Start"
"message": "Vor dem Starten des Spiels"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Wird vor dem Starten der Instanz ausgeführt."
"message": "Wird ausgeführt nach dem Starten des Spiels."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "Vor Spielstart auszuführender Befehl eingeben..."
"message": "Ausgeführter Befehl nach dem Starten des Spiels eingeben..."
},
"instance.settings.tabs.hooks.title": {
"message": "Start-Hooks"
"message": "Start Hooks"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Wrapper"
@@ -300,7 +237,7 @@
"message": "Neue Version wird installiert"
},
"instance.settings.tabs.installation.currently-installed": {
"message": "Derzeit installiert"
"message": "Aktuell installiert"
},
"instance.settings.tabs.installation.debug-information": {
"message": "Informationen für die Fehlerbehebung:"
@@ -315,7 +252,7 @@
"message": "Installieren"
},
"instance.settings.tabs.installation.install.in-progress": {
"message": "Installation im Gange"
"message": "Wird installiert"
},
"instance.settings.tabs.installation.loader-version": {
"message": "{loader} Version"
@@ -342,13 +279,13 @@
"message": "Modpack wird neu installiert"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "Eine Neuinstallation setzt alle installierten oder geänderten Inhalte auf den Zustand zurück, der vom Modpack bereitgestellt wird, und entfernt alle Mods oder Inhalte, die du zusätzlich zur ursprünglichen Installation hinzugefügt hast.\nDies kann unerwartetes Verhalten beheben, falls Änderungen an der Instanz vorgenommen wurden. Wenn deine Welten jedoch von zusätzlich installierten Inhalten abhängen, kann dies bestehende Welten beschädigen."
"message": "Durch die Neuinstallation werden alle installierten oder geänderten Inhalte auf die vom Modpack bereitgestellten Inhalte zurückgesetzt, wobei alle Mods oder Inhalte entfernt werden, die zusätzlich zur ursprünglichen Installation hinzugefügt wurden. Dies kann unerwartetes Verhalten beheben, wenn Änderungen an der Instanz vorgenommen wurden. Wenn Ihre Welten jedoch von zusätzlich installierten Inhalten abhängig sind, kann dies zu Fehlern in bestehenden Welten führen."
},
"instance.settings.tabs.installation.reinstall.confirm.title": {
"message": "Bist du dir sicher, dass du diese Instanz neu installieren willst?"
},
"instance.settings.tabs.installation.reinstall.description": {
"message": "Setzt den Inhalt der Instanz auf den ursprünglichen Zustand zurück und entfernt alle Mods oder Inhalte, die du zusätzlich zum ursprünglichen Modpack hinzugefügt hast."
"message": "Setzt den Inhalt der Instanz auf seinen ursprünglichen Zustand zurück und entfernt alle Mods oder Inhalte, die zusätzlich zum ursprünglichen Modpack hinzugefügt wurden."
},
"instance.settings.tabs.installation.reinstall.title": {
"message": "Modpack neu installieren"
@@ -369,7 +306,7 @@
"message": "Reparatur im Gange"
},
"instance.settings.tabs.installation.reset-selections": {
"message": "Auf aktuellen Stand zurücksetzen"
"message": "Auf aktuellen Wert zurücksetzen"
},
"instance.settings.tabs.installation.show-all-versions": {
"message": "Alle Versionen anzeigen"
@@ -378,19 +315,19 @@
"message": "Version ändern"
},
"instance.settings.tabs.installation.tooltip.action.install": {
"message": "Installieren"
"message": "installieren"
},
"instance.settings.tabs.installation.tooltip.action.reinstall": {
"message": "Neuinstallieren"
"message": "neuinstallieren"
},
"instance.settings.tabs.installation.tooltip.action.repair": {
"message": "Reparieren"
"message": "reparieren"
},
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
"message": "{action} während der Installation nicht möglich"
},
"instance.settings.tabs.installation.tooltip.cannot-while-offline": {
"message": "{action} offline nicht möglich"
"message": "{action} wegen fehlender Internetverbindung nicht möglich"
},
"instance.settings.tabs.installation.tooltip.cannot-while-repairing": {
"message": "{action} während der Reparation nicht möglich"
@@ -402,7 +339,7 @@
"message": "Verknüpfung der Instanz trennen"
},
"instance.settings.tabs.installation.unlink.confirm.description": {
"message": "Wenn du fortfährst, kannst du sie nicht erneut verknüpfen, ohne eine völlig neue Instanz zu erstellen. Du wirst keine Modpack-Updates mehr erhalten, und sie wird zu einer normalen Instanz."
"message": "Wenn du fortfährst, kann die Instanz nicht erneut verknüpft werden, ohne eine völlig neue Instanz zu erstellen. Du erhältst keine Modpack-Updates mehr und es wird zu einer normalen Instanz."
},
"instance.settings.tabs.installation.unlink.confirm.title": {
"message": "Möchtest du die Verknüpfungen dieser Instanz wirklich trennen?"
@@ -426,19 +363,19 @@
"message": "Java-Argumente"
},
"instance.settings.tabs.java.java-installation": {
"message": "Java-Installation"
"message": "Java Installation"
},
"instance.settings.tabs.java.java-memory": {
"message": "Zugewiesener Arbeitsspeicher"
},
"instance.settings.tabs.window": {
"message": "Fenster"
"message": "Spielfenster"
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "Benutzerdefinierte Fenstereinstellungen"
"message": "Benutzerdefinierte Spielfenstereinstellungen"
},
"instance.settings.tabs.window.fullscreen": {
"message": "Vollbild"
"message": "Vollbildschirm"
},
"instance.settings.tabs.window.fullscreen.description": {
"message": "Lässt das Spiel im Vollbildmodus starten (mit Verwendung von options.txt)."
@@ -447,7 +384,7 @@
"message": "Höhe"
},
"instance.settings.tabs.window.height.description": {
"message": "Die Höhe des Spielfensters beim Start."
"message": "Die Höhe des spiel Fensters beim Starten."
},
"instance.settings.tabs.window.height.enter": {
"message": "Höhe eingeben..."
@@ -456,7 +393,7 @@
"message": "Breite"
},
"instance.settings.tabs.window.width.description": {
"message": "Die Breite des Spielfensters beim Start."
"message": "Die Breite des spiel Fensters beim Starten."
},
"instance.settings.tabs.window.width.enter": {
"message": "Breite eingeben..."
@@ -507,7 +444,7 @@
"message": "Einzelspieler"
},
"instance.worlds.view_instance": {
"message": "Instanz anzeigen"
"message": "Instanzen anzeigen"
},
"instance.worlds.world_in_use": {
"message": "Welt wird aktuell benutzt"

View File

@@ -1,10 +1,4 @@
{
"app.auth-servers.unreachable.body": {
"message": "Minecraft authentication servers may be down right now. Check your internet connection and try again later."
},
"app.auth-servers.unreachable.header": {
"message": "Cannot reach authentication servers"
},
"app.settings.developer-mode-enabled": {
"message": "Developer mode enabled."
},

View File

@@ -65,69 +65,6 @@
"app.update.reload-to-update": {
"message": "Recarga para instalar la actualización"
},
"friends.action.add-friend": {
"message": "Añadir un amigo"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {solicitud} other {solicitudes}} de amistad"
},
"friends.add-friend.submit": {
"message": "Enviar solicitud de amistad"
},
"friends.add-friend.title": {
"message": "Añadiendo un amigo"
},
"friends.add-friend.username.description": {
"message": "¡Podría ser distinto a su nombre de Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Escribe el nombre de usuario de Modrinth..."
},
"friends.add-friend.username.title": {
"message": "¿Cuál es el nombre de usuario de Modrinth de tu amigo?"
},
"friends.add-friends-to-share": {
"message": "<link>¡Añade amigos</link> para ver qué están jugando!"
},
"friends.friend.cancel-request": {
"message": "Cancelar solicitud"
},
"friends.friend.remove-friend": {
"message": "Eliminar amigo"
},
"friends.friend.request-sent": {
"message": "Solicitud de amistad enviada"
},
"friends.friend.view-profile": {
"message": "Ver perfil"
},
"friends.heading": {
"message": "Amigos"
},
"friends.heading.active": {
"message": "Activos"
},
"friends.heading.offline": {
"message": "Desconectados"
},
"friends.heading.online": {
"message": "Conectados"
},
"friends.heading.pending": {
"message": "Pendiente"
},
"friends.no-friends-match": {
"message": "Ningún amigo coincide con \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Buscar amigos..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>¡Inicia sesión en una cuenta de Modrinth</link> para añadir amigos y ver qué están jugando!"
},
"instance.add-server.add-and-play": {
"message": "Añadir y jugar"
},
@@ -180,7 +117,7 @@
"message": "Servidor de Minecraft"
},
"instance.server-modal.resource-pack": {
"message": "Resource pack"
"message": "Paquete de recursos"
},
"instance.settings.tabs.general": {
"message": "General"

View File

@@ -24,13 +24,13 @@
"message": "Gestión de recursos"
},
"app.update-toast.body": {
"message": "¡La versión v{version} de Modrinth está lista para instalarse! Actualiza ahora o automáticamente al cerrar la aplicación."
"message": "¡La aplicación Modrinth v{version} está lista para instalarse! Actualiza ahora o automáticamente al cerrar la aplicación Modrinth."
},
"app.update-toast.body.download-complete": {
"message": "La descarga de la versión v{version} de Modrinth ha finalizado. Actualice ahora o automáticamente al cerrar la aplicación."
"message": "La descarga de la aplicación Modrinth v{version} ha finalizado. Actualice ahora o automáticamente al cerrar la aplicación Modrinth."
},
"app.update-toast.body.metered": {
"message": "¡La versión v{version} de Modrinth ya está disponible! Como estás conectado a una red con límite de datos, no la hemos descargado automáticamente."
"message": "¡La aplicación Modrinth v{version} ya está disponible! Como estás conectado a una red con límite de datos, no la hemos descargado automáticamente."
},
"app.update-toast.changelog": {
"message": "Registro de cambios"
@@ -65,69 +65,6 @@
"app.update.reload-to-update": {
"message": "Recarga para instalar la actualización"
},
"friends.action.add-friend": {
"message": "Agrega a un amigo"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {petición} other {peticiones}} de amistad"
},
"friends.add-friend.submit": {
"message": "Envía una petición de amistad"
},
"friends.add-friend.title": {
"message": "Agrega a un amigo"
},
"friends.add-friend.username.description": {
"message": "¡Puede ser diferente de su apodo en Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Escribe su apodo en Modrinth..."
},
"friends.add-friend.username.title": {
"message": "¿Cuál es el apodo de tu amigo en Modrinth?"
},
"friends.add-friends-to-share": {
"message": "<link>Añade amigos</link> para ver a qué están jugando."
},
"friends.friend.cancel-request": {
"message": "Cancelar petición"
},
"friends.friend.remove-friend": {
"message": "Eliminar amigo"
},
"friends.friend.request-sent": {
"message": "Petición de amistad enviada"
},
"friends.friend.view-profile": {
"message": "Ver perfil"
},
"friends.heading": {
"message": "Amigos"
},
"friends.heading.active": {
"message": "Activos"
},
"friends.heading.offline": {
"message": "Desconectados"
},
"friends.heading.online": {
"message": "Conectados"
},
"friends.heading.pending": {
"message": "Pendientes"
},
"friends.no-friends-match": {
"message": "Ningún apodo concuerda con ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "Buscando amigos..."
},
"friends.section.heading": {
"message": "{title}: {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Inicia sesión en una cuenta Modrinth</link> para añadir amigos y ver a qué están jugando."
},
"instance.add-server.add-and-play": {
"message": "Añadir y jugar"
},
@@ -204,7 +141,7 @@
"message": "No puedes duplicar mientras se instala."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "Duplicar instancia"
"message": "Duplicar instancia."
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "Crea una copia de esta instancia, incluyendo mundos, configuraciones, mods, etc."
@@ -279,7 +216,7 @@
"message": "Instancia"
},
"instance.settings.tabs.installation.change-version.already-installed.modded": {
"message": "{platform} {version} para Minecraft {game_version} ya está instalada"
"message": "{platform} {version} para Minecraft {game_version} ya esta instalada"
},
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
"message": "La versión vanilla {game_version} ya está instalada"

View File

@@ -23,116 +23,32 @@
"app.settings.tabs.resource-management": {
"message": "Pamamahala ng resource"
},
"app.update-toast.body": {
"message": "Ang Modrinth App v{version} ay handa nang ma-install. Mag-reload upang ma-update ngayon, o awtomatiko pagsara ng Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Tapos nang ma-download ang Modrinth App v{version}. Mag-reload upang ma-update ngayon, o awtomatiko pagsara ng Modrinth App."
"message": "Tapos nang ma-download ang Modrinth App v{version}. Mag-reload upang ma-update ngayon, o mamaya nalang sa pagsara ng Modrinth App."
},
"app.update-toast.body.metered": {
"message": "Magagamit na ngayon ang Modrinth App v{version}! Hindi namin dinanload kaagad dahil naka-metro ang inyong network."
},
"app.update-toast.changelog": {
"message": "Changelog"
},
"app.update-toast.download": {
"message": "I-download ({size})"
},
"app.update-toast.downloading": {
"message": "Nagda-download..."
},
"app.update-toast.reload": {
"message": "Mag-reload"
},
"app.update-toast.title": {
"message": "May bagong update"
},
"app.update-toast.title.download-complete": {
"message": "Nakumpleto ang pagdownload"
},
"app.update.complete-toast.text": {
"message": "Magpindot rito upang matingnan ang changelog."
},
"app.update.complete-toast.title": {
"message": "Tagumpay na na-install ang bersiyong {version}!"
},
"app.update.download-update": {
"message": "I-download ang update"
},
"app.update.downloading-update": {
"message": "Nagdadownload ng update ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Handang ma-install ang update"
},
"friends.action.add-friend": {
"message": "Magdagdag ng kaibigan"
},
"friends.action.view-friend-requests": {
"message": "{count, plural, one {{count}} other {{count} na}} hiling na makipagkaibigan"
},
"friends.add-friend.submit": {
"message": "Magpadala ng hiling na makipagkaibigan"
},
"friends.add-friend.title": {
"message": "Pagdaragdag ng kaibigan"
},
"friends.add-friend.username.description": {
"message": "Maaaraing hindi pareho sa kanilang pangalan sa Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Ilagay ang username sa Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Ano ang pangalan ng iyong kaibigan sa Modrinth?"
},
"friends.add-friends-to-share": {
"message": "<link>Magdagdag ng mga kaibigan</link> upang makita ang kanilang nilalaro!"
},
"friends.friend.cancel-request": {
"message": "Kanselahin ang hiling"
},
"friends.friend.remove-friend": {
"message": "Tanggalin ang kaibing"
},
"friends.friend.request-sent": {
"message": "Ipinadala na ang hiling na makipagkaibigan"
},
"friends.friend.view-profile": {
"message": "Tingnan ang profile"
},
"friends.heading": {
"message": "Mga kaibigan"
},
"friends.heading.active": {
"message": "Aktibo"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Nakabinbin"
},
"friends.no-friends-match": {
"message": "Walang kaibigang tumugma sa \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Hanapin ang mga kaibigan..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Mag-sign-in sa Modrinth account</link> upang maidagdag ang mga kaibigan at malaman ang kanilang nilalaro!"
},
"instance.add-server.add-and-play": {
"message": "Idagdag at laruin"
},
"instance.add-server.add-server": {
"message": "Idagdag ang server"
"message": "Idagdag ang serber"
},
"instance.add-server.resource-pack.disabled": {
"message": "Hindi pinahihintulotan"
@@ -144,10 +60,10 @@
"message": "Magpahintulot"
},
"instance.add-server.title": {
"message": "Magdagdag ng server"
"message": "Magdagdag ng serber"
},
"instance.edit-server.title": {
"message": "Baguhin ang server"
"message": "Baguhin ang serber"
},
"instance.edit-world.hide-from-home": {
"message": "Huwag ipakita sa Home na pahina"
@@ -159,7 +75,7 @@
"message": "Minecraft na Mundo"
},
"instance.edit-world.reset-icon": {
"message": "I-reset ang ikono"
"message": "Walain ang ikono"
},
"instance.edit-world.title": {
"message": "Baguhin ang mundo"
@@ -170,21 +86,15 @@
"instance.filter.updates-available": {
"message": "May bagong mga update"
},
"instance.server-modal.address": {
"message": "Adres"
},
"instance.server-modal.name": {
"message": "Pangalan"
},
"instance.server-modal.placeholder-name": {
"message": "Minecraft Server"
"message": "Minecraft na Serber"
},
"instance.server-modal.resource-pack": {
"message": "Resource pack"
},
"instance.settings.tabs.general": {
"message": "General"
},
"instance.settings.tabs.general.delete": {
"message": "I-delete ang instansiya"
},
@@ -194,18 +104,6 @@
"instance.settings.tabs.general.delete.description": {
"message": "Permanenteng matatanggal ang instansiya sa iyong device, kasali na ang iyong mga mundo, konpigurasyon, at lahat ng nakainstall na kontento. Mag-ingat, kapag magtanggal ka ng instansiya ay hindi na ito mababawi."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Nagde-delete..."
},
"instance.settings.tabs.general.duplicate-button": {
"message": "I-duplicate"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Hindi makaka-duplicate habang nag-i-install."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "I-duplicate ang instansiya"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "Gagawan ng kopya ng instansiyang ito, kasali na ang mga mundo, konpigurasyon, mods, at iba pa."
},
@@ -215,259 +113,19 @@
"instance.settings.tabs.general.edit-icon.remove": {
"message": "Tanggalin ang ikono"
},
"instance.settings.tabs.general.edit-icon.replace": {
"message": "Palitan ang ikono"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "Pumili ng ikono"
},
"instance.settings.tabs.general.library-groups": {
"message": "Mga grupo ng librerya"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "Gumawa ng bagong grupo"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "Binibigyan ng mga grupo ng librerya na iyong maayos ang iyong mga instansiya sa iba't-ibang pangkat in iyong librerya."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "Ilagay ang pangalan ng grupo"
},
"instance.settings.tabs.general.name": {
"message": "Pangalan"
},
"instance.settings.tabs.hooks": {
"message": "Mga launch hook"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "Mga custom na launch hook"
},
"instance.settings.tabs.hooks.description": {
"message": "Binibigyan-daan ng mga hook ang mga ekspertong user na makapagtakbo ng mga system command bago at pagkatapos ma-launch ang laro."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Post-exist"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "Ipapatakbo pagkatapos magsara ang laro."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Ilagay ang post-exit command..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Pre-launch"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Ipapatakbo bago mai-launch ang instansiya."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "Ilagay ang pre-launch command..."
},
"instance.settings.tabs.hooks.title": {
"message": "Mga launch hook ng laro"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Wrapper"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "Wrapper command sa pag-launch ng Minecraft."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Ilagay ang wrapper command..."
},
"instance.settings.tabs.installation": {
"message": "Instalasyon"
},
"instance.settings.tabs.installation.change-version.already-installed.modded": {
"message": "Naka-install naman ang {platform} {version} para sa Minecraft {game_version}"
},
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
"message": "Naka-install naman ang Vanilla {game_version}"
},
"instance.settings.tabs.installation.change-version.button": {
"message": "Palitan ang bersiyon"
},
"instance.settings.tabs.installation.change-version.button.install": {
"message": "I-install"
},
"instance.settings.tabs.installation.change-version.button.installing": {
"message": "Ini-install"
},
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
"message": "Nagfe-fetch ng mga bersiyon ng modpack"
},
"instance.settings.tabs.installation.change-version.in-progress": {
"message": "Ini-install ang bagong bersiyon"
},
"instance.settings.tabs.installation.currently-installed": {
"message": "Kasalukuyang naka-install"
},
"instance.settings.tabs.installation.debug-information": {
"message": "Impormasyon sa pagdebug:"
},
"instance.settings.tabs.installation.game-version": {
"message": "Bersiyon ng laro"
},
"instance.settings.tabs.installation.install": {
"message": "I-install"
},
"instance.settings.tabs.installation.install.in-progress": {
"message": "Nag-i-install ngayon"
},
"instance.settings.tabs.installation.loader-version": {
"message": "Bersiyon ng {loader}"
},
"instance.settings.tabs.installation.minecraft-version": {
"message": "Minecraft {version}"
},
"instance.settings.tabs.installation.no-loader-versions": {
"message": "Hindi magagamit ang {loader} sa Minecraft {version}. Sumubok ng ibang mod loader."
},
"instance.settings.tabs.installation.platform": {
"message": "Plataporma"
},
"instance.settings.tabs.installation.reinstall.button": {
"message": "I-reinstall ang modpack"
},
"instance.settings.tabs.installation.reinstall.button.reinstalling": {
"message": "Ini-re-reinstall ang modpack"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "Ang pagrere-install ay maaaring mare-reset ang lahat ng na-install o binago na kontento sa kung anong ibibigay ng modpack, tatanggalin ang mga mods o kontentong idinagdag mo lalo na ang mga orihinal na modpack. Maaari nitong masiayos ang mga hindi inaasahang pag-uugali kung may pagbabagong naganap sa instansiya, ngunit kung dumedepende na ang iyong mundo sa karagdagang kontento, maaari nitong masira ang mga umiiral na mundo."
},
"instance.settings.tabs.installation.reinstall.description": {
"message": "I-reset ang mga kontento ng instansiya sa orihinal niyang estado, tatanggalin ang mga mods o kontentong idinagdag mo lalo na ang mga orihinal na modpack."
},
"instance.settings.tabs.installation.reinstall.title": {
"message": "I-reinstall ang modpack"
},
"instance.settings.tabs.installation.repair.button": {
"message": "Ayusin"
},
"instance.settings.tabs.installation.repair.button.repairing": {
"message": "Inaayos"
},
"instance.settings.tabs.installation.repair.confirm.description": {
"message": "Sa pagre-repair, mare-reinstall ang mga dependency ng Minecraft at maghahanap ng mga kurapsiyon. Maaaring maresolbe nito ang mga isyu kung hindi malu-launch ang laro dahil sa mga launcher-related error, ngunit hindi nito mareresolbe ang mga isyu o pag-crash na dulot nga mga na-install na mod."
},
"instance.settings.tabs.installation.repair.confirm.title": {
"message": "Ayusin ang instansiya?"
},
"instance.settings.tabs.installation.repair.in-progress": {
"message": "Inaayos ngayon"
},
"instance.settings.tabs.installation.reset-selections": {
"message": "Sa kasalukuyan i-reset"
},
"instance.settings.tabs.installation.show-all-versions": {
"message": "Ipakita ang lahat ng bersiyon"
},
"instance.settings.tabs.installation.tooltip.action.change-version": {
"message": "palitan ang bersiyon"
},
"instance.settings.tabs.installation.tooltip.action.install": {
"message": "i-install"
},
"instance.settings.tabs.installation.tooltip.action.reinstall": {
"message": "i-reinstall"
},
"instance.settings.tabs.installation.tooltip.action.repair": {
"message": "ayusin"
},
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
"message": "Hindi makaka-{action} habang nag-i-install"
},
"instance.settings.tabs.installation.tooltip.cannot-while-offline": {
"message": "Hindi makaka-{action} habang nasa offline"
},
"instance.settings.tabs.installation.tooltip.cannot-while-repairing": {
"message": "Hindi makaka-{action} habang nag-aayos"
},
"instance.settings.tabs.installation.unknown-version": {
"message": "(hindi kilalang bersiyon)"
},
"instance.settings.tabs.installation.unlink.button": {
"message": "I-unlink sa instansiya"
},
"instance.settings.tabs.installation.unlink.confirm.description": {
"message": "Kapag ipagpatuloy mo, hindi mo na itong mai-link muli ng hindi gagawa ng bagong instansiya. Hindi ka makakatanggap ng mga update ng modpack at magiging normal na itong."
},
"instance.settings.tabs.installation.unlink.description": {
"message": "Ang instansiyang ito ay naka-link sa isang modpack, ibig sabihin ang mga mod ay hindi mai-update at hindi mo mapapalitan ang mod loader o ang bersiyon ng Minecraft."
},
"instance.settings.tabs.installation.unlink.title": {
"message": "I-unlink sa modpack"
},
"instance.settings.tabs.java": {
"message": "Java at memorya"
},
"instance.settings.tabs.java.hooks": {
"message": "Mga hook"
},
"instance.settings.tabs.java.java-arguments": {
"message": "Mga java argument"
},
"instance.settings.tabs.java.java-installation": {
"message": "Instalasyon ng Java"
},
"instance.settings.tabs.java.java-memory": {
"message": "Memoryang inilaan"
},
"instance.settings.tabs.window": {
"message": "Window"
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "Mga setting sa custom na window"
},
"instance.settings.tabs.window.fullscreen": {
"message": "Fullscreen"
},
"instance.settings.tabs.window.height": {
"message": "Taas"
},
"instance.settings.tabs.window.height.enter": {
"message": "Ilagay ang taas..."
},
"instance.settings.tabs.window.width": {
"message": "Lapad"
},
"instance.settings.tabs.window.width.enter": {
"message": "Ilagay ang lapad..."
},
"instance.settings.title": {
"message": "Mga Setting"
},
"instance.worlds.a_minecraft_server": {
"message": "Isang Minecraft Server"
},
"instance.worlds.copy_address": {
"message": "Kopyahin ang adres"
},
"instance.worlds.dont_show_on_home": {
"message": "Huwag ipakita sa Home"
},
"instance.worlds.filter.available": {
"message": "Magagamit"
},
"instance.worlds.hardcore": {
"message": "Modong eksperto"
},
"instance.worlds.play_instance": {
"message": "Laruin ang instansiya"
},
"instance.worlds.type.server": {
"message": "Server"
},
"instance.worlds.type.singleplayer": {
"message": "Pang-isahang laro"
},
"instance.worlds.view_instance": {
"message": "Tingnan ang instansiya"
},
"instance.worlds.world_in_use": {
"message": "Ginagamit ang mundo"
},
"search.filter.locked.instance.sync": {
"message": "Maki-sync sa instansiya"
}
}

View File

@@ -2,9 +2,6 @@
"app.settings.developer-mode-enabled": {
"message": "Mode développeur activé."
},
"app.settings.downloading": {
"message": "Téléchargement de la version {version}"
},
"app.settings.tabs.appearance": {
"message": "Apparence"
},
@@ -23,21 +20,6 @@
"app.settings.tabs.resource-management": {
"message": "Gestion des ressources"
},
"app.update-toast.body": {
"message": "L'application Modrinth v{version} est prêtes à être installé ! Relancez l'application pour faire la mise à jour maintenant ou la mise à jour se fera automatiquement lorsque vous fermerez l'application Modrinth."
},
"app.update-toast.body.download-complete": {
"message": "L'application Modrinth v{version} a finis d'être téléchargé ! Relancez l'application pour faire la mise à jour maintenant ou la mise à jour se fera automatiquement lorsque vous fermerez l'application Modrinth."
},
"app.update-toast.body.metered": {
"message": "L'application Modrinth v{version} est disponible dès maintenant ! Lorsque vous êtes sur un réseau limité ou en donnée mobile, nous ne téléchargerons pas les mises à jour automatiquement."
},
"app.update-toast.changelog": {
"message": "Notes de changement"
},
"app.update-toast.download": {
"message": "Télécharger ({size})"
},
"app.update-toast.downloading": {
"message": "Téléchargement..."
},
@@ -50,84 +32,9 @@
"app.update-toast.title.download-complete": {
"message": "Téléchargement terminé"
},
"app.update.complete-toast.text": {
"message": "Cliquez ici pour voir les changements récents."
},
"app.update.complete-toast.title": {
"message": "La version {version} a été téléchargée avec succès !"
},
"app.update.download-update": {
"message": "Télécharger la mise à jour"
},
"app.update.downloading-update": {
"message": "Téléchargement de la mise à jour ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Relancez l'application pour installer la mise à jour"
},
"friends.action.add-friend": {
"message": "Ajouter un ami"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural,one {demande}other {demandes}} d'ami"
},
"friends.add-friend.submit": {
"message": "Envoyer une demande d'ami"
},
"friends.add-friend.title": {
"message": "Ajouter un ami"
},
"friends.add-friend.username.description": {
"message": "Ça peut être différent de son pseudo Minecraft !"
},
"friends.add-friend.username.placeholder": {
"message": "Entrez un pseudo Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Quel est le pseudo Modrinth de votre ami ?"
},
"friends.add-friends-to-share": {
"message": "<link>Ajouter des amis</link> pour voir à quoi ils jouent !"
},
"friends.friend.cancel-request": {
"message": "Annuler la demande"
},
"friends.friend.remove-friend": {
"message": "Supprimer l'ami"
},
"friends.friend.request-sent": {
"message": "La demande d'ami a été envoyé"
},
"friends.friend.view-profile": {
"message": "Voir le profile"
},
"friends.heading": {
"message": "Amis"
},
"friends.heading.active": {
"message": "Actif"
},
"friends.heading.offline": {
"message": "Hors ligne"
},
"friends.heading.online": {
"message": "En ligne"
},
"friends.heading.pending": {
"message": "En attente"
},
"friends.no-friends-match": {
"message": "Aucuns amis ne correspondent à \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Chercher des amis..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Créer un compte Modrinth</link> pour ajouter des amis et voir à quoi ils jouent !"
},
"instance.add-server.add-and-play": {
"message": "Ajouter et jouer"
},

View File

@@ -2,9 +2,6 @@
"app.settings.developer-mode-enabled": {
"message": "מצב מפתח מופעל."
},
"app.settings.downloading": {
"message": "מוריד גרסה {version}"
},
"app.settings.tabs.appearance": {
"message": "מראה"
},
@@ -23,57 +20,6 @@
"app.settings.tabs.resource-management": {
"message": "ניהול משאבים"
},
"app.update-toast.body": {
"message": "Modrinth App גרסה: {version} מוכנה להורדה!\nרענן כדי להוריד עכשיו, או באופן אוטומטי כאשר תסגור את האפליקציה."
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App גרסה {version} סיימה את תהליך ההורדה. רענן כדי לעדכן עכשיו, או באופן אוטומטי כאשר תסגור את האפליקציה."
},
"app.update-toast.body.metered": {
"message": "אפליקצית מודרינת' גרסה {version} זמינה עכשיו! מכיוון שאתה על נתונים, אנחנו לא הורדנו אותה אוטומטית."
},
"app.update-toast.changelog": {
"message": "יומן שינויים"
},
"app.update-toast.download": {
"message": "הורד ({size})"
},
"app.update-toast.downloading": {
"message": "מוריד..."
},
"app.update-toast.reload": {
"message": "רענן"
},
"app.update-toast.title": {
"message": "עדכונים זמינים"
},
"app.update-toast.title.download-complete": {
"message": "הורדה הושלמה"
},
"app.update.complete-toast.text": {
"message": "לחץ כאן כדי לראות את יומן השינויים."
},
"app.update.complete-toast.title": {
"message": "גרסה {version} הותקנה בהצלחה!"
},
"app.update.download-update": {
"message": "הורד עדכון"
},
"app.update.downloading-update": {
"message": "מוריד עדכון ({percent}%)"
},
"app.update.reload-to-update": {
"message": "רענן בכדי להתקין את העדכונים"
},
"friends.add-friends-to-share": {
"message": "<link>הוסף חברים</link> כדי לראות במה הם משחקים!"
},
"friends.heading": {
"message": "חברים"
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"instance.add-server.add-and-play": {
"message": "הוסף ושחק"
},
@@ -105,7 +51,7 @@
"message": "עולם מיינקראפט"
},
"instance.edit-world.reset-icon": {
"message": פס סמל"
"message": יפוס אייקון"
},
"instance.edit-world.title": {
"message": "ערוך עולם"
@@ -138,7 +84,7 @@
"message": "מחק התקנה"
},
"instance.settings.tabs.general.delete.description": {
"message": "מוחק לצמיתות את התקנה זו מהמכשיר שלך, כולל העולמות שלך, הגדרות, וכל התוכן המותקן. שים לב, לאחר מחיקת ההתקנה אין דרך להחזיר אותה."
"message": "מוחק לצמיתות את התקנה זו מהמכשיר שלך, כולל העולמות שלך, הגדרות, וכל התוכן המותקן. תיזהר, מכיוון שלאחר מחיקת התקנה אין דרך להחזיר אותה."
},
"instance.settings.tabs.general.deleting.button": {
"message": "מוחק..."
@@ -153,19 +99,19 @@
"message": "שכפל התקנה"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "יוצר עותק של התקנה זו, כולל עולמות, הגדרות, מודים, וכדומה."
"message": "יוצר עותק של התקנה זו, כולל עולם, הגדרות, מודים, וכו."
},
"instance.settings.tabs.general.edit-icon": {
"message": "ערוך סמל"
"message": "ערוך אייקון"
},
"instance.settings.tabs.general.edit-icon.remove": {
"message": "הסר סמל"
"message": "הסר אייקון"
},
"instance.settings.tabs.general.edit-icon.replace": {
"message": "החלף סמל"
"message": "החלף אייקון"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "בחר סמל"
"message": "בחר אייקון"
},
"instance.settings.tabs.general.library-groups": {
"message": "קבוצות ספרייה"
@@ -174,7 +120,7 @@
"message": "צור קבוצה חדשה"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "קבוצות ספרייה מאפשרות לך לארגן את ההתקנות שלך לחלקים שונים בספרייה שלך."
"message": "קבוצות ספרייה מאפשרות לך לארגן את ההתקנות שלך לפי חלקים שונים."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "הכנס שם קבוצה"
@@ -186,7 +132,7 @@
"message": "פעולות בעת הפעלה"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "פעולות בעת הפעלה מותאמות אישית"
"message": "מותאם אישית"
},
"instance.settings.tabs.hooks.description": {
"message": "פעולות בעת הפעלה מאפשרות למשתמשים מתקדמים להריץ פקודות מערכת מסוימות לפני ואחרי שהמשחק מופעל."
@@ -201,16 +147,16 @@
"message": "הכנס פקודה לאחר יציאה..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "טרום-הפעלה"
"message": "טרום-השקה"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "מופעל לפני שההתקנה מופעלת."
"message": "מופעל לפני שהאינסטנס מתחיל."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "הכנס פקודה לפני ההפעלה..."
},
"instance.settings.tabs.hooks.title": {
"message": עולות הפעלה"
"message": קודות הפעלה מותאמות"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "מעטפת"
@@ -228,7 +174,7 @@
"message": "{platform} {version} בשביל מיינקראפט {game_version} כבר מותקן"
},
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
"message": "וונילה {game_version} כבר מותקן"
"message": "ונילה {game_version} כבר מותקן"
},
"instance.settings.tabs.installation.change-version.button": {
"message": "שנה גרסה"
@@ -240,7 +186,7 @@
"message": "מתקין"
},
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
"message": "מאחזר גרסאות חבילת מודים"
"message": "אחזור גרסאות מודפאקים"
},
"instance.settings.tabs.installation.change-version.in-progress": {
"message": "מתקין גרסה חדשה"
@@ -252,7 +198,7 @@
"message": "מידע ניפוי שגיאות:"
},
"instance.settings.tabs.installation.fetching-modpack-details": {
"message": "מאחזר פרטי חבילת מודים"
"message": "אחזור פרטי מודפאקים"
},
"instance.settings.tabs.installation.game-version": {
"message": "גרסת משחק"
@@ -270,13 +216,13 @@
"message": "מיינקראפט {version}"
},
"instance.settings.tabs.installation.no-connection": {
"message": "לא ניתן לאחזר את פרטי חבילת המודים המקושרת. אנא בדוק את חיבור האינטרנט שלך."
"message": "לא ניתן לאחזר את פרטי המודפאק המקושר. אנא בדוק את חיבור האינטרנט שלך."
},
"instance.settings.tabs.installation.no-loader-versions": {
"message": "{loader} אינו זמין עבור מיינקראפט {version}. אנא נסה טוען מודים אחר."
"message": "{loader}\" אינו זמין עבור מיינקראפט {version}. נסה מעלה מודים אחר."
},
"instance.settings.tabs.installation.no-modpack-found": {
"message": "התקנה זאת מקושרת לחבילת מודים, אך חבילת המודים לא נמצאה במודרינת'."
"message": "אינסטנס זה מקושר למודפק, אך המודפק לא נמצא ב-Modrinth."
},
"instance.settings.tabs.installation.platform": {
"message": "פלטפורמה"
@@ -288,13 +234,13 @@
"message": "מתקין מחדש את חבילת המודים"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "התקנה מחדש תאפס את כל התוכן המותקן או ששונה, ותחזיר אותו למצב שסופק על ידי חבילת המודים, תוך הסרת כל מוד או תוכן שהוספתם מעבר להתקנה המקורית. פעולה זו עשויה לתקן תקלות שנגרמו משינויים בהתקנה, אך אם העולמות שלכם תלויים בתוכן נוסף שהותקן, היא עלולה לשבש אותם."
"message": "התקנה מחדש תאפס את כל התוכן המותקן או ששונה, ותחזיר אותו למצב שסופק על ידי חבילת המודים, תוך הסרת כל מוד או תוכן שהוספתם מעבר להתקנה המקורית. פעולה זו עשויה לתקן תקלות שנגרמו משינויים במופע, אך אם העולמות שלכם תלויים בתוכן נוסף שהותקן, היא עלולה לשבש אותם."
},
"instance.settings.tabs.installation.reinstall.confirm.title": {
"message": "האם אתה בטוח שברצונך להתקין מחדש instance זה?"
},
"instance.settings.tabs.installation.reinstall.description": {
"message": "מאפס את התוכן שנמצא בהתקנה למצבו המקורי, תוך הסרת כל מוד או תוכן ששונה מעבר לחבילת המודים המקורית."
"message": "מאפס את התוכן שנמצא ב-instance למצבו המקורי, תוך הסרת כל מוד או תוכן שהוספת מעבר למודפק המקורי."
},
"instance.settings.tabs.installation.reinstall.title": {
"message": "התקן מחדש חבילת מודים"
@@ -348,16 +294,16 @@
"message": "לבטל את הקישור של ההתקנה הזו"
},
"instance.settings.tabs.installation.unlink.confirm.description": {
"message": "אם תמשיך, לא תוכל לקשר אותה מחדש מבלי ליצור התקנה חדשה לחלוטין. לא תקבל עוד עדכונים לחבילת המודים, והיא תהפוך להתקנה רגילה."
"message": "אם תמשיך, לא תוכל לקשר אותו מחדש מבלי ליצור instance חדש לחלוטין. לא תקבל עוד עדכונים למודפקים, והוא יהפוך לרגיל."
},
"instance.settings.tabs.installation.unlink.confirm.title": {
"message": "האם אתה בטוח שברצונך לנתק את הקישור ל-instance הזה?"
},
"instance.settings.tabs.installation.unlink.description": {
"message": "התקנה זאת מקושרת לחבילת מודים, מה שאומר שלא ניתן לעדכן מודים ולא ניתן לשנות את טוען המודים או את גרסת המיינקראפט. הניתוק יבטל לצמיתות את הקישור של התקנה זאת לחבילת המודים."
"message": "Instance זה מקושר ל-modpack, מה שאומר שלא ניתן לעדכן מודים ולא ניתן לשנות את ה-mod loader או את גרסת מיינקראפט. הניתוק יבטל לצמיתות את הקישור של instance זה מה-modpack."
},
"instance.settings.tabs.installation.unlink.title": {
"message": "נתק קישור מחבילת מודים"
"message": "נתק קישור מ-modpack"
},
"instance.settings.tabs.java": {
"message": "ג'אווה וזיכרון"
@@ -437,12 +383,6 @@
"instance.worlds.no_contact": {
"message": "לא ניתן ליצור קשר עם השרת"
},
"instance.worlds.no_server_quick_play": {
"message": "אתה יכול רק לקפוץ ישירות לשרתים מהגרסא אלפא 1.0.5 ואלך"
},
"instance.worlds.no_singleplayer_quick_play": {
"message": "אתה יכול לקפוץ ישירות לעולמות החל מהגרסא 1.20 ומעלה"
},
"instance.worlds.play_instance": {
"message": "שחק בהתקנה"
},
@@ -464,9 +404,6 @@
"search.filter.locked.instance-game-version.title": {
"message": "גרסאת המשחק מסופקת על ידי ההתקנה"
},
"search.filter.locked.instance-loader.title": {
"message": "מטען מודים מסופק ע\"י ההתקן"
},
"search.filter.locked.instance.sync": {
"message": "סנכרן עם התקנה"
}

View File

@@ -3,7 +3,7 @@
"message": "Fejlesztői mód bekapcsolva."
},
"app.settings.downloading": {
"message": "v{version} letöltése"
"message": "Letöltés v{version}"
},
"app.settings.tabs.appearance": {
"message": "Megjelenés"
@@ -60,74 +60,11 @@
"message": "Frissítés letöltése"
},
"app.update.downloading-update": {
"message": "Frissítés letöltése ({percent}%)"
"message": "Frissítés folyamatban ({percent}%)"
},
"app.update.reload-to-update": {
"message": "A telepítéshez újraindítás szükséges"
},
"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"
},
"friends.add-friend.title": {
"message": "Barát hozzáadása"
},
"friends.add-friend.username.description": {
"message": "Más is lehet, mint a Minecraft felhasználóneve!"
},
"friends.add-friend.username.placeholder": {
"message": "Barát Modrinth felhasználóneve..."
},
"friends.add-friend.username.title": {
"message": "Mi a barátod Modrinth felhasználóneve?"
},
"friends.add-friends-to-share": {
"message": "<link>Vegyél fel barátokat</link>, hogy lásd mivel játszanak!"
},
"friends.friend.cancel-request": {
"message": "Barátkérelem visszavonása"
},
"friends.friend.remove-friend": {
"message": "Barát eltávolítása"
},
"friends.friend.request-sent": {
"message": "Barátkérelem elküldve"
},
"friends.friend.view-profile": {
"message": "Profil megtekintése"
},
"friends.heading": {
"message": "Barátok"
},
"friends.heading.active": {
"message": "Aktív"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Jóváhagyásra vár"
},
"friends.no-friends-match": {
"message": "Nincsen \"{query}\" nevű barátod"
},
"friends.search-friends-placeholder": {
"message": "Keresés a barátaid között..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Lépj be Modrinth fiókba</link>, hogy felvehess barátokat és lásd mivel játszanak!"
},
"instance.add-server.add-and-play": {
"message": "Hozzáadás és játék"
},

View File

@@ -2,9 +2,6 @@
"app.settings.developer-mode-enabled": {
"message": "Mode pengembang dihidupkan."
},
"app.settings.downloading": {
"message": "Mengunduh v{version}"
},
"app.settings.tabs.appearance": {
"message": "Tampilan"
},
@@ -23,111 +20,6 @@
"app.settings.tabs.resource-management": {
"message": "Manajemen sumber"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} siap dipasang! Muat ulang untuk memperbarui sekarang, atau secara otomatis saat Anda menutup Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} telah selesai mengunduh. Muat ulang untuk memperbarui sekarang, atau secara otomatis saat Anda menutup Modrinth App."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} sudah tersedia! Karena Anda saat ini sedang berada dalam jaringan terukur, kami tidak mengunduhnya secara otomatis."
},
"app.update-toast.changelog": {
"message": "Log perubahan"
},
"app.update-toast.download": {
"message": "Unduh ({size})"
},
"app.update-toast.downloading": {
"message": "Mengunduh..."
},
"app.update-toast.reload": {
"message": "Muat ulang"
},
"app.update-toast.title": {
"message": "Pembaruan tersedia"
},
"app.update-toast.title.download-complete": {
"message": "Selesai mengunduh"
},
"app.update.complete-toast.text": {
"message": "Klik di sini untuk melihat log perubahan."
},
"app.update.complete-toast.title": {
"message": "Versi {version} berhasil dipasang!"
},
"app.update.download-update": {
"message": "Unduh pembaruan"
},
"app.update.downloading-update": {
"message": "Mengunduh pembaruan ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Muat ulang untuk memasang pembaruan"
},
"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"
},
"friends.add-friend.title": {
"message": "Menambah teman"
},
"friends.add-friend.username.description": {
"message": "Ia mungkin memiliki nama yang berbeda dari nama pengguna Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Masukkan nama pengguna Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Apakah nama pengguna Modrinth teman Anda?"
},
"friends.add-friends-to-share": {
"message": "<link>Tambah teman</link> untuk melihat apa yang mereka mainkan!"
},
"friends.friend.cancel-request": {
"message": "Batalkan permintaan"
},
"friends.friend.remove-friend": {
"message": "Hapus teman"
},
"friends.friend.request-sent": {
"message": "Permintaan teman dikirim"
},
"friends.friend.view-profile": {
"message": "Lihat profil"
},
"friends.heading": {
"message": "Teman"
},
"friends.heading.active": {
"message": "Aktif"
},
"friends.heading.offline": {
"message": "Luring"
},
"friends.heading.online": {
"message": "Daring"
},
"friends.heading.pending": {
"message": "Menunggu"
},
"friends.no-friends-match": {
"message": "Tidak ada teman dengan nama \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Cari teman..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Masuk ke akun Modrinth</link> untuk menambah teman dan melihat apa yang mereka mainkan!"
},
"instance.add-server.add-and-play": {
"message": "Tambah dan mainkan"
},

View File

@@ -65,69 +65,6 @@
"app.update.reload-to-update": {
"message": "Ricarica per installare aggiornamento"
},
"friends.action.add-friend": {
"message": "Stringi un'amicizia"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {richiesta} other {richieste}} d'amicizia"
},
"friends.add-friend.submit": {
"message": "Invia richiesta d'amicizia"
},
"friends.add-friend.title": {
"message": "Stringendo l'amicizia"
},
"friends.add-friend.username.description": {
"message": "Potrebbe essere diverso dal nome utente di Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Inserisci il nome utente Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Con quale utente Modrinth vuoi stringere l'amicizia?"
},
"friends.add-friends-to-share": {
"message": "<link>Stringi un'amicizia</link> per sapere a cosa stanno giocando!"
},
"friends.friend.cancel-request": {
"message": "Annulla richiesta"
},
"friends.friend.remove-friend": {
"message": "Annulla amicizia"
},
"friends.friend.request-sent": {
"message": "Richiesta d'amicizia inviata"
},
"friends.friend.view-profile": {
"message": "Visita profilo"
},
"friends.heading": {
"message": "Amicizie"
},
"friends.heading.active": {
"message": "Attivo"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "In sospeso"
},
"friends.no-friends-match": {
"message": "Nessuna amicizia pertinente a ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "Cerca amicizie..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Accedi all'account Modrinth</link> per stringere amicizie e sapere a cosa stanno giocando!"
},
"instance.add-server.add-and-play": {
"message": "Aggiungi e gioca"
},

View File

@@ -2,11 +2,8 @@
"app.settings.developer-mode-enabled": {
"message": "開発者モードがオンになっています。"
},
"app.settings.downloading": {
"message": "v{version}をダウンロード中"
},
"app.settings.tabs.appearance": {
"message": "外観"
"message": "表示設定"
},
"app.settings.tabs.default-instance-options": {
"message": "インスタンスの基本設定"
@@ -15,119 +12,14 @@
"message": "機能設定"
},
"app.settings.tabs.java-installations": {
"message": "Javaのインストール"
"message": "Javaのインストール設定"
},
"app.settings.tabs.privacy": {
"message": "プライバシー"
"message": "プライバシー設定"
},
"app.settings.tabs.resource-management": {
"message": "リソース管理"
},
"app.update-toast.body": {
"message": "Modrinth App v{version}は今すぐインストールできます!再起動して今すぐ更新するか、アプリを閉じた際に自動で更新されます。"
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version}のダウンロードが完了しました。再起動して今すぐ更新するか、アプリを閉じた際に自動で更新されます。"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version}は今すぐダウンロードできます!従量課金制ネットワークを使用しているため自動でダウンロードはされていません。"
},
"app.update-toast.changelog": {
"message": "更新履歴"
},
"app.update-toast.download": {
"message": "ダウンロード ({size})"
},
"app.update-toast.downloading": {
"message": "ダウンロード中..."
},
"app.update-toast.reload": {
"message": "再起動"
},
"app.update-toast.title": {
"message": "アップデートが可能"
},
"app.update-toast.title.download-complete": {
"message": "ダウンロード完了"
},
"app.update.complete-toast.text": {
"message": "クリックすると更新履歴を表示できます。"
},
"app.update.complete-toast.title": {
"message": "バージョン {version} のインストールが正常に完了しました!"
},
"app.update.download-update": {
"message": "アップデートをダウンロード"
},
"app.update.downloading-update": {
"message": "アップデートをダウンロード中 ({percent}%)"
},
"app.update.reload-to-update": {
"message": "再起動して今すぐ更新"
},
"friends.action.add-friend": {
"message": "フレンドを追加"
},
"friends.action.view-friend-requests": {
"message": "{count}件の友達リクエスト"
},
"friends.add-friend.submit": {
"message": "フレンド申請を送信"
},
"friends.add-friend.title": {
"message": "フレンドを追加中"
},
"friends.add-friend.username.description": {
"message": "これはMinecraftユーザーネームと違う可能性があります"
},
"friends.add-friend.username.placeholder": {
"message": "Modrinthユーザーネームを入力..."
},
"friends.add-friend.username.title": {
"message": "あなたのフレンドのModrinthユーザーネームは何ですか"
},
"friends.add-friends-to-share": {
"message": "<link>友達を追加</link>して、彼らが何をしているか見てみよう!"
},
"friends.friend.cancel-request": {
"message": "申請をキャンセル"
},
"friends.friend.remove-friend": {
"message": "フレンドを削除"
},
"friends.friend.request-sent": {
"message": "フレンド申請が送信されました"
},
"friends.friend.view-profile": {
"message": "プロフィールを表示"
},
"friends.heading": {
"message": "フレンド"
},
"friends.heading.active": {
"message": "活動中"
},
"friends.heading.offline": {
"message": "オフライン"
},
"friends.heading.online": {
"message": "オンライン"
},
"friends.heading.pending": {
"message": "保留中"
},
"friends.no-friends-match": {
"message": "フレンド\"{query}\"は見つかりませんでした"
},
"friends.search-friends-placeholder": {
"message": "フレンドを検索…"
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Modrinthアカウントにサインイン</link>して友達を追加し、彼らがプレイしているゲームをチェックしよう!"
},
"instance.add-server.add-and-play": {
"message": "追加してプレイ"
},

View File

@@ -23,18 +23,6 @@
"app.settings.tabs.resource-management": {
"message": "리소스 관리"
},
"app.update-toast.changelog": {
"message": "변경사항"
},
"app.update-toast.download": {
"message": "다운로드 ({size})"
},
"app.update-toast.downloading": {
"message": "다운로드 중..."
},
"app.update-toast.reload": {
"message": "리로드"
},
"instance.add-server.add-and-play": {
"message": "추가하고 플레이"
},

View File

@@ -65,69 +65,6 @@
"app.update.reload-to-update": {
"message": "موات سمولا اونتوق مماسڠ کمس کيني"
},
"friends.action.add-friend": {
"message": "تمبه راکن"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, other {ڤرمينتاٴن}} راکن"
},
"friends.add-friend.submit": {
"message": "هانتر ڤرمينتاٴن راکن"
},
"friends.add-friend.title": {
"message": "منمبه راکن"
},
"friends.add-friend.username.description": {
"message": "اي موڠکين بربيذا درڤد نام ڤڠݢونا ماٴينکرف‌ت مريک!"
},
"friends.add-friend.username.placeholder": {
"message": "ماسوقکن نام ڤڠݢونا Modrinth..."
},
"friends.add-friend.username.title": {
"message": "اڤاکه نام ڤڠݢونا Modrinth راکن اندا؟"
},
"friends.add-friends-to-share": {
"message": "<link>تمبه راکن</link> اونتوق مليهت اڤ يڠ مريک ماءينکن!"
},
"friends.friend.cancel-request": {
"message": "بطلکن ڤرمينتاٴن"
},
"friends.friend.remove-friend": {
"message": "اليه کلوار راکن"
},
"friends.friend.request-sent": {
"message": "ڤرمينتاٴن راکن تله دهانتر"
},
"friends.friend.view-profile": {
"message": "ليهت ڤروفيل"
},
"friends.heading": {
"message": "راکن"
},
"friends.heading.active": {
"message": "اکتيف"
},
"friends.heading.offline": {
"message": "لوار تالين"
},
"friends.heading.online": {
"message": "دالم تالين"
},
"friends.heading.pending": {
"message": "منوڠݢو"
},
"friends.no-friends-match": {
"message": "تياد راکن يڠ سڤادن دڠن ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "چاري راکن..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>لوݢ ماسوق کأکاٴون Modrinth</link> اونتوق منمبه راکن دان مليهت اڤ يڠ مريک ماءينکن!"
},
"instance.add-server.add-and-play": {
"message": "تمبه دان ماءين"
},

View File

@@ -65,69 +65,6 @@
"app.update.reload-to-update": {
"message": "Muat semula untuk memasang kemas kini"
},
"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"
},
"friends.add-friend.title": {
"message": "Menambah rakan"
},
"friends.add-friend.username.description": {
"message": "Ia mungkin berbeza daripada nama pengguna Minecraft mereka!"
},
"friends.add-friend.username.placeholder": {
"message": "Masukkan nama pengguna Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Apakah nama pengguna Modrinth rakan anda?"
},
"friends.add-friends-to-share": {
"message": "<link>Tambah rakan</link> untuk melihat apa yang mereka mainkan!"
},
"friends.friend.cancel-request": {
"message": "Batalkan permintaan"
},
"friends.friend.remove-friend": {
"message": "Alih keluar rakan"
},
"friends.friend.request-sent": {
"message": "Permintaan rakan telah dihantar"
},
"friends.friend.view-profile": {
"message": "Lihat profil"
},
"friends.heading": {
"message": "Rakan"
},
"friends.heading.active": {
"message": "Aktif"
},
"friends.heading.offline": {
"message": "Luar Talian"
},
"friends.heading.online": {
"message": "Dalam Talian"
},
"friends.heading.pending": {
"message": "Menunggu"
},
"friends.no-friends-match": {
"message": "Tiada rakan yang sepadan dengan ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "Cari rakan..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Log masuk ke akaun Modrinth</link> untuk menambah rakan dan melihat apa yang mereka mainkan!"
},
"instance.add-server.add-and-play": {
"message": "Tambah dan main"
},

View File

@@ -2,9 +2,6 @@
"app.settings.developer-mode-enabled": {
"message": "Ontwikkelaarsmodus ingeschakeld."
},
"app.settings.downloading": {
"message": "v{version} wordt gedownload"
},
"app.settings.tabs.appearance": {
"message": "Uiterlijk"
},
@@ -23,111 +20,6 @@
"app.settings.tabs.resource-management": {
"message": "Bronnenbeheer"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} is klaar om geïnstalleerd te worden! Herlaad om nu te updaten, of automatisch wanneer je de Modrinth App afsluit."
},
"app.update-toast.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."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} is nu beschikbaar! Omdat je nu op een netwerk met datalimiet zit, is de download niet automatisch gestart."
},
"app.update-toast.changelog": {
"message": "Changelog"
},
"app.update-toast.download": {
"message": "Download ({size})"
},
"app.update-toast.downloading": {
"message": "Aan het downloaden..."
},
"app.update-toast.reload": {
"message": "Herlaad"
},
"app.update-toast.title": {
"message": "Update beschikbaar"
},
"app.update-toast.title.download-complete": {
"message": "Downloaden voltooid"
},
"app.update.complete-toast.text": {
"message": "Klik hier om de changelog te bekijken."
},
"app.update.complete-toast.title": {
"message": "Versie {version} is succesvol geïnstalleerd!"
},
"app.update.download-update": {
"message": "Download update"
},
"app.update.downloading-update": {
"message": "Update downloaden ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Herlaad om de update te installeren"
},
"friends.action.add-friend": {
"message": "Voeg een vriend toe"
},
"friends.action.view-friend-requests": {
"message": "{count} vriend {count, plural,one {verzoek} other {verzoeken}}"
},
"friends.add-friend.submit": {
"message": "Stuur een vriendschapsverzoek"
},
"friends.add-friend.title": {
"message": "Een vriend toevoegen"
},
"friends.add-friend.username.description": {
"message": "Het kan verschillen van hun Minecraft gebruikersnaam!"
},
"friends.add-friend.username.placeholder": {
"message": "Voer Modrinth gebruikersnaam in..."
},
"friends.add-friend.username.title": {
"message": "Wat is de Modrinth gebruikersnaam van uw vriend?"
},
"friends.add-friends-to-share": {
"message": "<link>Voeg vrienden</link> om te zien wat hun spelen!"
},
"friends.friend.cancel-request": {
"message": "Annuleer verzoek"
},
"friends.friend.remove-friend": {
"message": "Verwijder vriend"
},
"friends.friend.request-sent": {
"message": "Vriendschapsverzoek gestuurd"
},
"friends.friend.view-profile": {
"message": "Bekijk profiel"
},
"friends.heading": {
"message": "Vrienden"
},
"friends.heading.active": {
"message": "Actief"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "In behandeling"
},
"friends.no-friends-match": {
"message": "Geen vrienden die overeenkomen met \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Zoek vrienden..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Log in op een Modrinth account</link> om vrienden toe te voegen en te kijken wat zij spelen!"
},
"instance.add-server.add-and-play": {
"message": "Toevoegen en spelen"
},

View File

@@ -24,10 +24,7 @@
"message": "Zarządzanie zasobami"
},
"app.update-toast.body": {
"message": "Wersja Modrinth App v{version} jest gotowa do pobrania! Odśwież, żeby zaktualizować teraz, albo automatycznie, gdy zamkniesz Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Wersja Modrinth App v{version} została pobrana. Odśwież, żeby zaktualizować teraz, albo automatycznie, gdy zamkniesz Modrinth App."
"message": "Nowa wersja Modrinth v{version} jest gotowa do pobrania! Odśwież, żeby zaktualizować teraz, albo automatycznie, gdy zamkniesz aplikację Modrinth."
},
"app.update-toast.body.metered": {
"message": "Wersja v{version} Modrinth App jest dostępna! Skoro korzystasz z sieci taryfowej, nie pobraliśmy jej automatycznie."
@@ -65,69 +62,6 @@
"app.update.reload-to-update": {
"message": "Załaduj ponownie, aby zainstalować aktualizację"
},
"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"
},
"friends.add-friend.title": {
"message": "Dodawanie znajomego"
},
"friends.add-friend.username.description": {
"message": "Może różnić się od nazwy użytkownika Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Wpisz nazwę użytkownika Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Jaka jest nazwa użytkownika Twojego znajomego?"
},
"friends.add-friends-to-share": {
"message": "<link>Dodaj znajomych</link> by widzieć, w co grają!"
},
"friends.friend.cancel-request": {
"message": "Anuluj zaproszenie"
},
"friends.friend.remove-friend": {
"message": "Usuń z znajomych"
},
"friends.friend.request-sent": {
"message": "Wysłano zaproszenie"
},
"friends.friend.view-profile": {
"message": "Pokaż profil"
},
"friends.heading": {
"message": "Znajomi"
},
"friends.heading.active": {
"message": "Aktywny"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Oczekujące"
},
"friends.no-friends-match": {
"message": "Nie znaleziono znajomych pasujących do zapytania \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Szukaj znajomych..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Zaloguj się na konto Modrinth</link> by dodać znajomych i widzieć, w co grają!"
},
"instance.add-server.add-and-play": {
"message": "Dodaj i graj"
},

View File

@@ -1,6 +1,6 @@
{
"app.settings.developer-mode-enabled": {
"message": "Modo desenvolvedor."
"message": "Modo desenvolvedor ativado."
},
"app.settings.downloading": {
"message": "Baixando v{version}"
@@ -54,7 +54,7 @@
"message": "Clique aqui para ver as mudanças."
},
"app.update.complete-toast.title": {
"message": "Versão {version} instalada!"
"message": "Versão {version} instalada com sucesso!"
},
"app.update.download-update": {
"message": "Baixar atualização"
@@ -63,70 +63,7 @@
"message": "Baixando atualização ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Recarregue para instalar a atualização"
},
"friends.action.add-friend": {
"message": "Adicionar um amigo"
},
"friends.action.view-friend-requests": {
"message": "{count, plural, =0 {Nenhuma solicitação de amizade} one {{count} solicitação de amizade} other {{count} solicitações de amizade}}"
},
"friends.add-friend.submit": {
"message": "Enviar amizade"
},
"friends.add-friend.title": {
"message": "Adicionando amigo"
},
"friends.add-friend.username.description": {
"message": "Pode ser diferente do nome que ele usa no Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Insira o nome de usuário..."
},
"friends.add-friend.username.title": {
"message": "Qual o nome de usuário do seu amigo no Modrinth?"
},
"friends.add-friends-to-share": {
"message": "<link>Adicione amigos</link> para ver o que eles estão jogando!"
},
"friends.friend.cancel-request": {
"message": "Cancelar solicitação"
},
"friends.friend.remove-friend": {
"message": "Remover amigo"
},
"friends.friend.request-sent": {
"message": "Solicitação de amizade enviada"
},
"friends.friend.view-profile": {
"message": "Ver perfil"
},
"friends.heading": {
"message": "Amigos"
},
"friends.heading.active": {
"message": "Ativo"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Pendente"
},
"friends.no-friends-match": {
"message": "Nenhum amigo corresponde a \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Buscar amigos..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Inicie uma sessão com o Modrinth</link> para adicionar amigos e ver o que eles estão jogando!"
"message": "Recarregar para instalar a atualização"
},
"instance.add-server.add-and-play": {
"message": "Adicionar e jogar"
@@ -192,7 +129,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 poderá ser recuperada."
"message": "Apaga permanente a instância do seu dispositivo, incluindo seus mundos, configurações e todo o conteúdo instalado. Tenha cuidado, porque não será possível recuperá-la após apaga-la."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Excluindo..."
@@ -237,13 +174,13 @@
"message": "Nome"
},
"instance.settings.tabs.hooks": {
"message": "Ações de inicialização"
"message": "Gatilhos de inicialização"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "Ações de inicialização personalizadas"
"message": "Gatilho de inicialização personalizado"
},
"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": "Os Gatilhos permitem que usuários mais experientes executem comandos do sistema antes e depois de inicializar o jogo."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Ao sair"
@@ -252,10 +189,10 @@
"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 de pós-saída..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Pré-inicialização"
"message": "Antes de inicializar"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Executado antes que a instância seja inicializada."
@@ -264,7 +201,7 @@
"message": "Insira o comando a ser executado antes da inicialização..."
},
"instance.settings.tabs.hooks.title": {
"message": "Ações de inicialização do jogo"
"message": "Gatilhos de inicialização do jogo"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Comando auxiliar"
@@ -273,7 +210,7 @@
"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"
@@ -300,7 +237,7 @@
"message": "Instalando nova versão"
},
"instance.settings.tabs.installation.currently-installed": {
"message": "Versão instalada"
"message": "Instalado atualmente"
},
"instance.settings.tabs.installation.debug-information": {
"message": "Informação de depuração:"
@@ -420,7 +357,7 @@
"message": "Variáveis de ambiente"
},
"instance.settings.tabs.java.hooks": {
"message": "Ações"
"message": "Gatilhos"
},
"instance.settings.tabs.java.java-arguments": {
"message": "Argumentos do Java"

View File

@@ -2,9 +2,6 @@
"app.settings.developer-mode-enabled": {
"message": "Modo de desenvolvedor ativado."
},
"app.settings.downloading": {
"message": "A transferir v{version}"
},
"app.settings.tabs.appearance": {
"message": "Aparência"
},
@@ -23,111 +20,6 @@
"app.settings.tabs.resource-management": {
"message": "Gestão de recursos"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} está pronta para ser instalada! Recarrega para atualizar agora, ou automaticamente quando fechares a Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} acabou de ser transferida. Recarrega para atualizar agora, ou automaticamente quando fechares a Modrinth App."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} está disponível! Como estás numa rede com tráfego limitado, não a transferimos automaticamente."
},
"app.update-toast.changelog": {
"message": "Lista de alterações"
},
"app.update-toast.download": {
"message": "Transferir ({size})"
},
"app.update-toast.downloading": {
"message": "A transferir..."
},
"app.update-toast.reload": {
"message": "Recarregar"
},
"app.update-toast.title": {
"message": "Atualização disponível"
},
"app.update-toast.title.download-complete": {
"message": "Transferência concluída"
},
"app.update.complete-toast.text": {
"message": "Clica aqui para ver a lista de alterações."
},
"app.update.complete-toast.title": {
"message": "Versão {version} foi instalada com sucesso!"
},
"app.update.download-update": {
"message": "Transferir atualização"
},
"app.update.downloading-update": {
"message": "A transferir atualização ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Recarrega para instalar a atualização"
},
"friends.action.add-friend": {
"message": "Adicionar um amigo"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural,one {pedido} other {pedidos}} de amizade"
},
"friends.add-friend.submit": {
"message": "Enviar pedido de amizade"
},
"friends.add-friend.title": {
"message": "Adicionar um amigo"
},
"friends.add-friend.username.description": {
"message": "Pode ser diferente do nome no Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Insere o nome do utilizador no Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Qual é o nome de utilizador do teu amigo no Modrinth?"
},
"friends.add-friends-to-share": {
"message": "<link>Adiciona amigos</link> para ver o que estão a jogar!"
},
"friends.friend.cancel-request": {
"message": "Cancelar pedido"
},
"friends.friend.remove-friend": {
"message": "Remover amigo"
},
"friends.friend.request-sent": {
"message": "Pedido de amizade enviado"
},
"friends.friend.view-profile": {
"message": "Ver perfil"
},
"friends.heading": {
"message": "Amigos"
},
"friends.heading.active": {
"message": "Ativo"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Pendente"
},
"friends.no-friends-match": {
"message": "Nenhum amigo corresponde a \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Procurar amigos..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Inicia a sessão com uma conta Modrinth</link> para adicionar amigos e ver o que estão a jogar!"
},
"instance.add-server.add-and-play": {
"message": "Adicionar e jogar"
},

View File

@@ -12,7 +12,7 @@
"message": "Предустановки"
},
"app.settings.tabs.feature-flags": {
"message": "Флаги функций"
"message": "Экспериментальные функции"
},
"app.settings.tabs.java-installations": {
"message": "Установки Java"
@@ -30,7 +30,7 @@
"message": "Скачивание Modrinth App v{version} завершено. Перезапустите приложение, чтобы обновить его, или оно обновится автоматически при закрытии."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} доступно для скачивания! Используется лимитное подключение, поэтому скачивание не началось автоматически."
"message": "Modrinth App v{version} доступно к скачиванию! Используется лимитное подключение, поэтому скачивание не началось автоматически."
},
"app.update-toast.changelog": {
"message": "Список изменений"
@@ -63,70 +63,7 @@
"message": "Скачивание обновления ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Перезапустить и обновить"
},
"friends.action.add-friend": {
"message": "Добавить в друзья"
},
"friends.action.view-friend-requests": {
"message": "{count} {count, plural, one {запрос} few {запроса} many {запросов} other {запроса}} дружбы"
},
"friends.add-friend.submit": {
"message": "Отправить запрос дружбы"
},
"friends.add-friend.title": {
"message": "Добавление в друзья"
},
"friends.add-friend.username.description": {
"message": "Оно может быть не таким, как в Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Введите имя на Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Какое имя у друга на Modrinth?"
},
"friends.add-friends-to-share": {
"message": "<link>Добавьте друзей</link>, чтобы знать, во что они играют!"
},
"friends.friend.cancel-request": {
"message": "Отменить запрос"
},
"friends.friend.remove-friend": {
"message": "Удалить из друзей"
},
"friends.friend.request-sent": {
"message": "Отправлен запрос дружбы"
},
"friends.friend.view-profile": {
"message": "Открыть профиль"
},
"friends.heading": {
"message": "Друзья"
},
"friends.heading.active": {
"message": "В игре"
},
"friends.heading.offline": {
"message": "Не в сети"
},
"friends.heading.online": {
"message": "В сети"
},
"friends.heading.pending": {
"message": "В ожидании"
},
"friends.no-friends-match": {
"message": "Нет друзей по запросу «{query}»"
},
"friends.search-friends-placeholder": {
"message": "Поиск по друзьям..."
},
"friends.section.heading": {
"message": "{title} — {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Войдите в Modrinth</link>, чтобы добавлять друзей и знать, во что они играют!"
"message": "Перезапустить для обновления"
},
"instance.add-server.add-and-play": {
"message": "Добавить и играть"
@@ -147,7 +84,7 @@
"message": "Добавление сервера"
},
"instance.edit-server.title": {
"message": "Настройка сервера"
"message": "Изменение сервера"
},
"instance.edit-world.hide-from-home": {
"message": "Не показывать на главной"
@@ -162,7 +99,7 @@
"message": "Сбросить иконку"
},
"instance.edit-world.title": {
"message": "Изменение мира"
"message": "Изменение информации о мире"
},
"instance.filter.disabled": {
"message": "Отключённые"
@@ -237,13 +174,13 @@
"message": "Название"
},
"instance.settings.tabs.hooks": {
"message": "Команды запуска"
"message": "Настройки запуска"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "Изменение команд запуска"
},
"instance.settings.tabs.hooks.description": {
"message": "Позволяет опытным пользователям задать системные команды, выполняемые до и после запуска игры."
"message": "Позволяют опытным пользователям задать системные команды, выполняемые перед запуском и после закрытия игры."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "После выхода"
@@ -264,13 +201,13 @@
"message": "Введите команду перед запуском..."
},
"instance.settings.tabs.hooks.title": {
"message": "Команды запуска игры"
"message": "Настройки запуска игры"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Обёртка"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "Команда-обёртка для запуска Minecraft."
"message": "Команда обёртки для запуска Minecraft."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Команда обёртки..."
@@ -315,7 +252,7 @@
"message": "Установить"
},
"instance.settings.tabs.installation.install.in-progress": {
"message": "Выполняется установка"
"message": "Выполняется установка..."
},
"instance.settings.tabs.installation.loader-version": {
"message": "Версия {loader}"
@@ -366,7 +303,7 @@
"message": "Восстановить сборку?"
},
"instance.settings.tabs.installation.repair.in-progress": {
"message": "Выполняется исправление"
"message": "Выполняется исправление..."
},
"instance.settings.tabs.installation.reset-selections": {
"message": "Сбросить выбор"
@@ -420,7 +357,7 @@
"message": "Переменные среды"
},
"instance.settings.tabs.java.hooks": {
"message": "Команды запуска"
"message": "Настройки запуска"
},
"instance.settings.tabs.java.java-arguments": {
"message": "Аргументы Java"

View File

@@ -2,9 +2,6 @@
"app.settings.developer-mode-enabled": {
"message": "Utvecklarläge aktiverat."
},
"app.settings.downloading": {
"message": "Ladda ner v{version}"
},
"app.settings.tabs.appearance": {
"message": "Utseende"
},
@@ -23,111 +20,6 @@
"app.settings.tabs.resource-management": {
"message": "Resurshantering"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} är redo att laddas ner! Ladda om för att uppdatera nu, eller automatiskt när du stänger Modrinth App."
},
"app.update-toast.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."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} är nu tillgänglig! Eftersom du använder ett nätverk med datatrafikbegränsningar har vi inte laddat ner det automatiskt."
},
"app.update-toast.changelog": {
"message": "Ändringslogg"
},
"app.update-toast.download": {
"message": "Ladda ner ({size})"
},
"app.update-toast.downloading": {
"message": "Laddar ner..."
},
"app.update-toast.reload": {
"message": "Ladda om"
},
"app.update-toast.title": {
"message": "Uppdatering tillgänglig"
},
"app.update-toast.title.download-complete": {
"message": "Nedladdning slutförd"
},
"app.update.complete-toast.text": {
"message": "Tryck här för att visa ändringsloggen."
},
"app.update.complete-toast.title": {
"message": "Version {version} har installerats!"
},
"app.update.download-update": {
"message": "Ladda ner uppdatering"
},
"app.update.downloading-update": {
"message": "Laddar ner uppdatering ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Ladda om för att installera uppdatering"
},
"friends.action.add-friend": {
"message": "Lägg till en vän"
},
"friends.action.view-friend-requests": {
"message": "{count} vän{count, plural, one {förfrågan} other {förfrågningar}}"
},
"friends.add-friend.submit": {
"message": "Skicka vänförfrågan"
},
"friends.add-friend.title": {
"message": "Lägga till en vän"
},
"friends.add-friend.username.description": {
"message": "Det kan vara annorlunda från deras Minecraft-användarnamn!"
},
"friends.add-friend.username.placeholder": {
"message": "Ange Modrinth-användarnamn..."
},
"friends.add-friend.username.title": {
"message": "Vad är din väns Modrinth-användarnamn?"
},
"friends.add-friends-to-share": {
"message": "<link>Lägg till vänner</link> för att se vad de spelar!"
},
"friends.friend.cancel-request": {
"message": "Avbryt förfrågan"
},
"friends.friend.remove-friend": {
"message": "Ta bort vän"
},
"friends.friend.request-sent": {
"message": "Vänförfrågan skickad"
},
"friends.friend.view-profile": {
"message": "Visa profil"
},
"friends.heading": {
"message": "Vänner"
},
"friends.heading.active": {
"message": "Aktiva"
},
"friends.heading.offline": {
"message": "Offline"
},
"friends.heading.online": {
"message": "Online"
},
"friends.heading.pending": {
"message": "Väntar"
},
"friends.no-friends-match": {
"message": "Inga vänner matchar ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "Sök efter vänner..."
},
"friends.section.heading": {
"message": "{title}: {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Logga in på ett Modrinth-konto</link> för att lägga till vänner och se vad de spelar!"
},
"instance.add-server.add-and-play": {
"message": "Lägg till och spela"
},

View File

@@ -2,18 +2,9 @@
"app.settings.developer-mode-enabled": {
"message": "กำลังอยู่ในโหมดผู้พัฒนา"
},
"app.settings.downloading": {
"message": "ดาวน์โหลด เวอร์ชั่น{version}"
},
"app.settings.tabs.appearance": {
"message": "รูปลักษณ์"
},
"app.settings.tabs.default-instance-options": {
"message": "ตัวเลือกอินสแตนซ์เริ่มต้น"
},
"app.settings.tabs.feature-flags": {
"message": "ระบบควบคุมการเปิดใช้งานฟีเจอร์"
},
"app.settings.tabs.java-installations": {
"message": "การจัดการ Java ที่ติดตั้ง"
},
@@ -23,51 +14,6 @@
"app.settings.tabs.resource-management": {
"message": "การจัดการทรัพยากร"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} พร้อมติดตั้งแล้ว! รีโหลดเพื่ออัปเดตทันที หรือจะอัปเดตอัตโนมัติเมื่อคุณปิดแอป"
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} ดาวน์โหลดเสร็จแล้ว! รีโหลดเพื่ออัปเดตทันที หรือจะอัปเดตอัตโนมัติเมื่อคุณปิดแอป"
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} พร้อมให้ดาวน์โหลดแล้ว! เนื่องจากคุณกำลังใช้งานเครือข่ายที่มีการคิดค่าใช้จ่ายตามปริมาณข้อมูล ระบบจึงไม่ได้ดาวน์โหลดอัตโนมัติ"
},
"app.update-toast.changelog": {
"message": "บันทึกการเปลี่ยนแปลง"
},
"app.update-toast.download": {
"message": "ดาวน์โหลด ({size})"
},
"app.update-toast.downloading": {
"message": "กำลังดาวน์โหลด...."
},
"app.update-toast.reload": {
"message": "รีโหลด"
},
"app.update-toast.title": {
"message": "อัพเดตพร้อมแล้ว"
},
"app.update-toast.title.download-complete": {
"message": "ดาวน์โหลดเรียบร้อยแล้ว"
},
"app.update.complete-toast.text": {
"message": "คลิกที่นี่เพื่อดูบันทึกการเปลี่ยนแปลง"
},
"app.update.complete-toast.title": {
"message": "เวอร์ชั่น {version} ถูกติดตั้งแล้ว"
},
"app.update.download-update": {
"message": "ดาวน์โหลดอัพเดต"
},
"app.update.downloading-update": {
"message": "ดาวน์โหลดอัพเดตไปแล้ว ({percent}%)"
},
"app.update.reload-to-update": {
"message": "รีโหลดเพื่อติดตั้งอัพเดต"
},
"friends.heading": {
"message": "เพื่อน"
},
"instance.add-server.add-and-play": {
"message": "เพิ่มและเล่นทันที"
},
@@ -104,9 +50,6 @@
"instance.edit-world.title": {
"message": "แก้ไขโลก"
},
"instance.filter.disabled": {
"message": "ปิดโปรเจ็ค"
},
"instance.filter.updates-available": {
"message": "พบอัพเดท"
},
@@ -125,15 +68,6 @@
"instance.settings.tabs.general": {
"message": "ทั่วไป"
},
"instance.settings.tabs.general.delete": {
"message": "ลบอินสแตนซ์"
},
"instance.settings.tabs.general.delete.button": {
"message": "ลบอินสแตนซ์"
},
"instance.settings.tabs.general.delete.description": {
"message": "ลบอินสแตนซ์ออกจากอุปกรณ์ของคุณอย่างถาวร รวมถึงโลก การตั้งค่า และเนื้อหาทั้งหมดที่ติดตั้งไว้ โปรดระมัดระวัง เนื่องจากเมื่อถูกลบแล้วจะไม่สามารถกู้คืนได้อีก"
},
"instance.settings.tabs.general.deleting.button": {
"message": "กำลังลบ..."
},
@@ -143,12 +77,6 @@
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "ไม่สามารถทำซ้ำได้ขณะติดตั้ง"
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "สร้างสำเนาอินสแตนซ์"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "สร้างสำเนาของอินสแตนซ์นี้ รวมถึงโลก การตั้งค่า ม็อด และอื่นๆ"
},
"instance.settings.tabs.general.edit-icon": {
"message": "แก้ไขไอคอน"
},
@@ -167,9 +95,6 @@
"instance.settings.tabs.general.library-groups.create": {
"message": "สร้างกลุ่มใหม่"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "กลุ่มไลบรารีช่วยให้คุณจัดระเบียบอินสแตนซ์ของคุณเป็นหมวดหมู่ต่างๆ ภายในไลบรารี"
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "ใส่ชื่อกลุ่ม"
},

View File

@@ -9,7 +9,7 @@
"message": "Kiyas"
},
"app.settings.tabs.default-instance-options": {
"message": "Mga pagpipilian sa batayang tularan"
"message": "Mga pagpipilian sa batayang pangyayari"
},
"app.settings.tabs.feature-flags": {
"message": "Watawat ng mga tampok"
@@ -23,116 +23,32 @@
"app.settings.tabs.resource-management": {
"message": "Pamamahala ng mapagkukunan"
},
"app.update-toast.body": {
"message": "Handa nang maikabit ang Modrinth App v{version}. Muling dalhin upang maisapanahon, o pagkusaan pagpinid ng Modrinth App."
},
"app.update-toast.body.download-complete": {
"message": "Tapos nang maidalamba ang Modrinth App v{version}. Muling dalhin upang maisapanahon, o pagkusaan pagpinid ng Modrinth App."
"message": "Tapos nang maidalamba ang Modrinth App v{version}. Muling dalhin upang matakda ang pagbabago ngayon, o mamaya nalang sa pagpinid ng Modrinth App."
},
"app.update-toast.body.metered": {
"message": "Magagamit na ngayon ang Modrinth App v{version}! Hindi namin dinalamba kaagad dahil bilang ang inyong kabalagan."
},
"app.update-toast.changelog": {
"message": "Tala ng Pagbabago"
},
"app.update-toast.download": {
"message": "Idalamba ({size})"
},
"app.update-toast.downloading": {
"message": "Nagdadalamba..."
},
"app.update-toast.reload": {
"message": "Magmulindala"
},
"app.update-toast.title": {
"message": "May kamakailang pagsasapanahon"
},
"app.update-toast.title.download-complete": {
"message": "Natapos ang pagdalamba"
},
"app.update.complete-toast.text": {
"message": "Magpindot rito upang matingnan ang talaan ng pagbabago."
},
"app.update.complete-toast.title": {
"message": "Tagumpay na nakabit ang bersiyong {version}!"
},
"app.update.download-update": {
"message": "Idalamba ang pagbabago"
},
"app.update.downloading-update": {
"message": "Nagdadalamba ng pagsasapanahon ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Handang makabit ang pagsasapanahon"
},
"friends.action.add-friend": {
"message": "Magdagdag ng kaibigan"
},
"friends.action.view-friend-requests": {
"message": "{count, plural, one {{count}} other {{count} na}} hiling na makipagkaibigan"
},
"friends.add-friend.submit": {
"message": "Magpadala ng hiling na makipagkaibigan"
},
"friends.add-friend.title": {
"message": "Pagdaragdag ng kaibigan"
},
"friends.add-friend.username.description": {
"message": "Maaaraing hindi katulad sa kanilang pangalan sa Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Ilagay ang pangalan ng gagamit sa Modrinth... "
},
"friends.add-friend.username.title": {
"message": "Ano ang pangalan ng iyong kaibigan sa Modrinth?"
},
"friends.add-friends-to-share": {
"message": "<link>Magdagdag ng mga kaibigan</link> upang makita ang kanilang nilalaro!"
},
"friends.friend.cancel-request": {
"message": "Bawiin ang hiling"
},
"friends.friend.remove-friend": {
"message": "Tanggalin ang kaibing"
},
"friends.friend.request-sent": {
"message": "Ipinadala na ang hiling na makipagkaibigan"
},
"friends.friend.view-profile": {
"message": "Tingnan ang propayl"
},
"friends.heading": {
"message": "Mga kaibigan"
},
"friends.heading.active": {
"message": "Masigla"
},
"friends.heading.offline": {
"message": "Pinid"
},
"friends.heading.online": {
"message": "Bukas"
},
"friends.heading.pending": {
"message": "Nakabinbin"
},
"friends.no-friends-match": {
"message": "Walang kaibigang tumugma sa \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "Hanapin ang mga kaibigan..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Mag-sign-in sa Modrinth account</link> upang maidagdag ang mga kaibigan at malaman ang kanilang nilalaro!"
"message": "Nagdadalamba ng pagbabago ({percent}%)"
},
"instance.add-server.add-and-play": {
"message": "Idagdag at laruin"
},
"instance.add-server.add-server": {
"message": "Idagdag ang pansilbi"
"message": "Idagdag ang tagapagsilbi"
},
"instance.add-server.resource-pack.disabled": {
"message": "Hindi pinahihintulotan"
@@ -144,10 +60,10 @@
"message": "Magpahintulot"
},
"instance.add-server.title": {
"message": "Magdagdag ng pansilbi"
"message": "Magdagdag ng tagapagsilbi"
},
"instance.edit-server.title": {
"message": "Baguhin ang pansilbi"
"message": "Baguhin ang tagapagsilbi"
},
"instance.edit-world.hide-from-home": {
"message": "Huwag ipakita sa panig ng Bahay"
@@ -159,7 +75,7 @@
"message": "Minecraft na Daigdig"
},
"instance.edit-world.reset-icon": {
"message": "Isauli ang lambana"
"message": "Walain ang lambana"
},
"instance.edit-world.title": {
"message": "Baguhin ang daigdig"
@@ -170,41 +86,23 @@
"instance.filter.updates-available": {
"message": "May mga bagong pagbabago"
},
"instance.server-modal.address": {
"message": "Tinitirhan"
},
"instance.server-modal.name": {
"message": "Pangalan"
},
"instance.server-modal.placeholder-name": {
"message": "Minecraft na Pansilbi"
"message": "Minecraft na Tagapagsilbi"
},
"instance.server-modal.resource-pack": {
"message": "Balot ng mapagkukunan"
},
"instance.settings.tabs.general": {
"message": "Pangkalahatan"
},
"instance.settings.tabs.general.delete": {
"message": "Tanggalin ang tularan"
"message": "Tanggalin ang pangyayari"
},
"instance.settings.tabs.general.delete.button": {
"message": "Tanggalin ang tularan"
"message": "Tanggalin ang pangyayari"
},
"instance.settings.tabs.general.delete.description": {
"message": "Habam buhay na matatanggal ang tularan sa iyong pakasam, kasali na ang iyong mga daigdig, pagsasaayos, at lahat ng kinabitang nilalaman. Mag-ingat, hindi na mababawi kapag nagtanggal ka na ng tularan."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Binubura..."
},
"instance.settings.tabs.general.duplicate-button": {
"message": "Isaduhasipi"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Hindi makakasaduhasipi habang nagkakabit."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "Isaduhasipi ang tularan"
"message": "Lagiang matatanggal ang pangyayari sa iyong pakasam, kasali na ang iyong mga daigdig, pagsasaayos, at lahat ng kinabitang nilalaman. Mag-ingat, hindi na mababawi kapag magtanggal ka ng pangyayari."
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "Gagawan ng sipi ang pangyayaring ito, kasali na ang mga daigdig, pagsasaaayos, pagbabago, at iba pa."
@@ -215,259 +113,19 @@
"instance.settings.tabs.general.edit-icon.remove": {
"message": "Tanggalin ang lambana"
},
"instance.settings.tabs.general.edit-icon.replace": {
"message": "Palitan ang lambana"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "Pumili ng lambana"
},
"instance.settings.tabs.general.library-groups": {
"message": "Mga pangkat ng aklatan"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "Gumawa ng bagong pangkat"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "Binibigyang-daan ng mga pangkat ng aklatan na iyong maayos ang iyong mga tularan sa iba't-ibang tapyas ng iyong aklatan."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "Ilagay ang pangalan ng pangkat"
},
"instance.settings.tabs.general.name": {
"message": "Pangalan"
},
"instance.settings.tabs.hooks": {
"message": "Mga kawit sa paglunsad"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "Mga pasadyang kawit sa paglunsad"
},
"instance.settings.tabs.hooks.description": {
"message": "Binibigyan-daan ng mga kawit ang mga madalubhasang gumagamit na makapagtakbo ng mga utos sa kaayusan bago at pagkatapos malunsad ang laro."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Tapos-mapinid"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "Ipapatakbo pagkatapos mapinid ang laro."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Ilagay ang tapos-mapinid na utos..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Bago-malunsad"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Ipapatakbo bago mailunsad ang tularan."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "Ilagay ang bago-malunsad na utos..."
},
"instance.settings.tabs.hooks.title": {
"message": "Mga kawit sa paglunsad ng laro"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Pambalot"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "Pambalot na utos sa paglunsad ng Minecraft."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Ilagay ang pambalot na utos..."
},
"instance.settings.tabs.installation": {
"message": "Pagkakabit"
},
"instance.settings.tabs.installation.change-version.already-installed.modded": {
"message": "Naka-install naman ang {platform} {version} para sa Minecraft {game_version}"
},
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
"message": "Nakakabit naman ang Baynilyang {game_version}"
},
"instance.settings.tabs.installation.change-version.button": {
"message": "Palitan ang bersiyon"
},
"instance.settings.tabs.installation.change-version.button.install": {
"message": "Ikabit"
},
"instance.settings.tabs.installation.change-version.button.installing": {
"message": "Kinakabit"
},
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
"message": "Naghahanap ng mga bersiyon ng mga balot ng pambago"
},
"instance.settings.tabs.installation.change-version.in-progress": {
"message": "Kinakabit ang bagong bersiyon"
},
"instance.settings.tabs.installation.currently-installed": {
"message": "Kasalukuyang nakakabit"
},
"instance.settings.tabs.installation.debug-information": {
"message": "Kaalaman sa pagdalisay:"
},
"instance.settings.tabs.installation.game-version": {
"message": "Bersiyon ng laro"
},
"instance.settings.tabs.installation.install": {
"message": "Ikabit"
},
"instance.settings.tabs.installation.install.in-progress": {
"message": "Nagkakabit ngayon"
},
"instance.settings.tabs.installation.loader-version": {
"message": "Bersiyon ng {loader}"
},
"instance.settings.tabs.installation.minecraft-version": {
"message": "Minecraft {version}"
},
"instance.settings.tabs.installation.no-loader-versions": {
"message": "Hindi magagamit ang {loader} sa Minecraft {version}. Sumubok ng ibang tagapagtala ng pambago."
},
"instance.settings.tabs.installation.platform": {
"message": "Batyawan"
},
"instance.settings.tabs.installation.reinstall.button": {
"message": "Ikabit muli ang balot ng pambago"
},
"instance.settings.tabs.installation.reinstall.button.reinstalling": {
"message": "Kinakabit muli ang balot ng pambago"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "Maaaring mababalik sa dati ang lahat ng kinabit o binago na nilalaman sa kung anong ibibigay ng balot ng pambago, tatanggalin ang mga pambago o nilalamang idinagdag mo lalo na ang nangunang balot ng pambago. Maaari nitong masiayos ang mga hindi inaasahang pag-uugali kung may pagbabagong naganap sa tularan, ngunit kung nakabatay na ang iyong daigdig sa karagdagang nilalaman, maaari nitong masira ang mga umiiral na daigdig."
"message": "Maaaring mababalik sa dati ang lahat ng kinabit o binago na nilalaman sa kung anong ibibigay ng balot ng pambago, tatanggalin ang mga pambago o nilalamang idinagdag mo lalo na ang nangunang balot ng pambago. Maaari nitong masiayos ang mga hindi inaasahang pag-uugali kung may pagbabagong naganap sa pangyayari, ngunit kung nakabatay na ang iyong daigdig sa karagdagang nilalaman, maaari nitong masira ang mga umiiral na daigdig."
},
"instance.settings.tabs.installation.reinstall.description": {
"message": "Ibalik ang mga nilalaman ng tularan sa pangunahing kalagayan, tatanggalin ang mga pambago o nilalamang idinagdag mo lalo na ang nangunang balot ng pambago."
},
"instance.settings.tabs.installation.reinstall.title": {
"message": "Ikabit muli ang balot ng pambago"
},
"instance.settings.tabs.installation.repair.button": {
"message": "Kumpunihin"
},
"instance.settings.tabs.installation.repair.button.repairing": {
"message": "Kinukumpuni"
},
"instance.settings.tabs.installation.repair.confirm.description": {
"message": "Sa pag-aayos, magkakabit muli ng mga sandalan ng Minecraft at maghahanap ng mga katiwalian. Maaaring malutas nito ang mga isyu kung hindi malulunsad ang laro dahil sa mga kamalian sa tagapaglunsad, ngunit hindi nito malulutas ang mga isyu o pagbagsak na dulot nga mga pambagong nakakabit."
},
"instance.settings.tabs.installation.repair.confirm.title": {
"message": "Kumpunihin ang tularan?"
},
"instance.settings.tabs.installation.repair.in-progress": {
"message": "Kinukumpuni ngayon"
},
"instance.settings.tabs.installation.reset-selections": {
"message": "Sa kasalukuyan isauli"
},
"instance.settings.tabs.installation.show-all-versions": {
"message": "Ipakita ang lahat ng bersiyon"
},
"instance.settings.tabs.installation.tooltip.action.change-version": {
"message": "palitan ang bersiyon"
},
"instance.settings.tabs.installation.tooltip.action.install": {
"message": "ikabit"
},
"instance.settings.tabs.installation.tooltip.action.reinstall": {
"message": "ikabit muli"
},
"instance.settings.tabs.installation.tooltip.action.repair": {
"message": "kumpunihin"
},
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
"message": "Hindi makaka-{action} habang nagkakabit"
},
"instance.settings.tabs.installation.tooltip.cannot-while-offline": {
"message": "Hindi makaka-{action} habang di-nakakabit"
},
"instance.settings.tabs.installation.tooltip.cannot-while-repairing": {
"message": "Hindi makaka-{action} habang nagkukumpuni"
},
"instance.settings.tabs.installation.unknown-version": {
"message": "(hindi kilalang bersiyon)"
},
"instance.settings.tabs.installation.unlink.button": {
"message": "Paghiwalayin sa tularan"
},
"instance.settings.tabs.installation.unlink.confirm.description": {
"message": "Kung ipapatuloy mo, hindi mo na itong maaaring maikawing muli ng hindi lilikha ng panibagong tularan. Hindi ka makatatanggap ng mga pagsasapanahon ng pambalot ng pambago at magiging pangkaraniwan na itong."
},
"instance.settings.tabs.installation.unlink.description": {
"message": "Nakakawing ang tularang ito sa isang pambalot ng pambago, ibig sabihin hindi maisapanahon ang mga pambago at hindi mo mapapalitan ang tagapagdala ng pambago o ang bersiyon ng Minecraft."
},
"instance.settings.tabs.installation.unlink.title": {
"message": "Paghiwalayin sa balot ng pambago"
},
"instance.settings.tabs.java": {
"message": "Java at memorya"
},
"instance.settings.tabs.java.hooks": {
"message": "Mga kawit"
},
"instance.settings.tabs.java.java-arguments": {
"message": "Mga sadyansukat ng java"
},
"instance.settings.tabs.java.java-installation": {
"message": "Kabit ng Java"
},
"instance.settings.tabs.java.java-memory": {
"message": "Memoryang inilaan"
},
"instance.settings.tabs.window": {
"message": "Durungawan"
"message": "Ibalik ang mga nilalaman ng pangyayari sa pangunahing kalagayan, tatanggalin ang mga pambago o nilalamang idinagdag mo lalo na ang nangunang balot ng pambago."
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "Mga kagustuhan sa pasadyang durungawan"
},
"instance.settings.tabs.window.fullscreen": {
"message": "Buong-tabing"
},
"instance.settings.tabs.window.height": {
"message": "Tayog"
},
"instance.settings.tabs.window.height.enter": {
"message": "Ilagay ang taas..."
},
"instance.settings.tabs.window.width": {
"message": "Lapad"
},
"instance.settings.tabs.window.width.enter": {
"message": "Ilagay ang lapad..."
},
"instance.settings.title": {
"message": "Mga Kagustuhan"
},
"instance.worlds.a_minecraft_server": {
"message": "Isang Minecraft na Pansilbi"
},
"instance.worlds.copy_address": {
"message": "Sipiin ang tinitirhan"
},
"instance.worlds.dont_show_on_home": {
"message": "Huwag ipakita sa Tirahan"
},
"instance.worlds.filter.available": {
"message": "Magagamit"
},
"instance.worlds.hardcore": {
"message": "Paraang pangdalubhasan"
},
"instance.worlds.play_instance": {
"message": "Laruin ang tularan"
},
"instance.worlds.type.server": {
"message": "Pansilbi"
},
"instance.worlds.type.singleplayer": {
"message": "Pang-isahang laro"
},
"instance.worlds.view_instance": {
"message": "Tingnan ang tularan"
},
"instance.worlds.world_in_use": {
"message": "Ginagamit ang daigdig"
},
"search.filter.locked.instance.sync": {
"message": "Makipagsabayan sa tularan"
"message": "Tingnan ang pangyayari"
}
}

View File

@@ -3,7 +3,7 @@
"message": "Geliştirici modu açıldı."
},
"app.settings.downloading": {
"message": "v{version} indiriliyor"
"message": "V{version} İndiriliyor"
},
"app.settings.tabs.appearance": {
"message": "Görünüm"
@@ -65,69 +65,6 @@
"app.update.reload-to-update": {
"message": "Güncellemeyi yüklemek için yeniden yükleyin"
},
"friends.action.add-friend": {
"message": "Bir arkadaş ekle"
},
"friends.action.view-friend-requests": {
"message": "{count} Arkadaşlık {count, plural,one {isteği} other {istekleri}}"
},
"friends.add-friend.submit": {
"message": "Arkadaşlık isteği gönder"
},
"friends.add-friend.title": {
"message": "Bir arkadaş ekleme"
},
"friends.add-friend.username.description": {
"message": "Minecraft kullanıcı adlarından farklı olabilir!"
},
"friends.add-friend.username.placeholder": {
"message": "Modrinth kullanıcı adını girin..."
},
"friends.add-friend.username.title": {
"message": "Arkadaşının Modrinth kullanıcı adı nedir?"
},
"friends.add-friends-to-share": {
"message": "Arkadaşlarının ne oynadığını görmek için <link>onları ekle</link>!"
},
"friends.friend.cancel-request": {
"message": "İsteği iptal et"
},
"friends.friend.remove-friend": {
"message": "Arkadaşı çıkar"
},
"friends.friend.request-sent": {
"message": "Arkadaşlık isteği gönderildi"
},
"friends.friend.view-profile": {
"message": "Profili gör"
},
"friends.heading": {
"message": "Arkadaşlar"
},
"friends.heading.active": {
"message": "Aktif"
},
"friends.heading.offline": {
"message": "Çevrimdışı"
},
"friends.heading.online": {
"message": "Çevrimiçi"
},
"friends.heading.pending": {
"message": "Bekleniyor"
},
"friends.no-friends-match": {
"message": "''{query}'' ile eşleşen arkadaş yok"
},
"friends.search-friends-placeholder": {
"message": "Arkadaşları ara..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "Arkadaş eklemek ve ne oynadıklarını görmek için bir <link>Modrinth hesabına giriş yap</link>!"
},
"instance.add-server.add-and-play": {
"message": "Ekle ve oyna"
},

View File

@@ -92,9 +92,6 @@
"instance.settings.tabs.installation.change-version.button.installing": {
"message": "Утырту"
},
"instance.settings.tabs.installation.debug-information": {
"message": "Төзәтү мәгълүматы:"
},
"instance.settings.tabs.installation.game-version": {
"message": "Уен версиясе"
},

View File

@@ -24,16 +24,16 @@
"message": "Керування ресурсами"
},
"app.update-toast.body": {
"message": "Modrinth App v{version} готова до встановлення! Перезапустіть, щоб оновити зараз. Або, оновлення буде здійснено автоматично, коли закриєте Modrinth App."
"message": "Modrinth App v{version} готова до встановлення. Перезапустіть програму, щоб встановити зараз, чи вона оновиться автоматично після закриття."
},
"app.update-toast.body.download-complete": {
"message": "Modrinth App v{version} уже завантажилась. Перезапустіть, щоб оновити зараз. Або, оновлення буде здійснено автоматично, коли закриєте Modrinth App."
"message": "Завантаження Modrinth App v{version} завершене. Перезапустіть програму щоб оновити зараз, чи вона оновиться автоматично після закриття."
},
"app.update-toast.body.metered": {
"message": "Modrinth App v{version} доступна зараз! Оскільки ви на лімітному з’єднанні, ми не завантажили її автоматично."
"message": "Modrinth App v{version} вже доступна до завантаження! Оскільки ви на лімітному підключенні, ми не завантажили його автоматично."
},
"app.update-toast.changelog": {
"message": "Журнал змін"
"message": "Список змін"
},
"app.update-toast.download": {
"message": "Завантажити ({size})"
@@ -65,69 +65,6 @@
"app.update.reload-to-update": {
"message": "Перезавантажте, щоб установити оновлення"
},
"friends.action.add-friend": {
"message": "Додати друга"
},
"friends.action.view-friend-requests": {
"message": "{count} запит{count, plural, one { у} few {и в} many {ів у} other {у в}} друзі"
},
"friends.add-friend.submit": {
"message": "Надіслати запит у друзі"
},
"friends.add-friend.title": {
"message": "Додавання друга"
},
"friends.add-friend.username.description": {
"message": "Може відрізнятися від його або її імені користувача Minecraft!"
},
"friends.add-friend.username.placeholder": {
"message": "Уведіть ім’я користувача Modrinth…"
},
"friends.add-friend.username.title": {
"message": "Яке ім’я користувача Modrinth у вашого друга?"
},
"friends.add-friends-to-share": {
"message": "<link>Додайте друзів</link>, щоб бачити, у що вони грають!"
},
"friends.friend.cancel-request": {
"message": "Скасувати запит"
},
"friends.friend.remove-friend": {
"message": "Видалити друга"
},
"friends.friend.request-sent": {
"message": "Запит у друзі надіслано"
},
"friends.friend.view-profile": {
"message": "Переглянути профіль"
},
"friends.heading": {
"message": "Друзі"
},
"friends.heading.active": {
"message": "Активний"
},
"friends.heading.offline": {
"message": "Поза мережею"
},
"friends.heading.online": {
"message": "У мережі"
},
"friends.heading.pending": {
"message": "Очікується"
},
"friends.no-friends-match": {
"message": "Немає друзів, котрі збігаються з «{query}»"
},
"friends.search-friends-placeholder": {
"message": "Шукати друзів…"
},
"friends.section.heading": {
"message": "{title} — {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Увійдіть в обліковий запис Modrinth</link>, щоб додати друзів і бачити, у що вони грають!"
},
"instance.add-server.add-and-play": {
"message": "Додати та грати"
},
@@ -186,7 +123,7 @@
"message": "Загальні"
},
"instance.settings.tabs.general.delete": {
"message": "Видалити примірник"
"message": "Видалити інсталяцію"
},
"instance.settings.tabs.general.delete.button": {
"message": "Видалити профіль"
@@ -342,7 +279,7 @@
"message": "Перевстановлення збірки"
},
"instance.settings.tabs.installation.reinstall.confirm.description": {
"message": "Перевстановлення скине увесь доданий або модифікований вміст до наданого збіркою, прибираючи будь-які, додатково встановлені вами модифікації чи вміст.\nЯкщо ви змінювали інсталяцію, це може виправити непередбачувану поведінку, однак, якщо ваші світи залежать від додаткового вмісту, це може зламати ці світи."
"message": "Перевстановлення скине увесь доданий або модифікований вміст до наданого збіркою, прибираючи будь-які, додатково встановлені вами модифікації чи вміст.\nЯкщо ви змінювали профіль, це може виправити непередбачувану поведінку, однак, якщо ваші світи залежать від додаткового вмісту, це може зламати ці світи."
},
"instance.settings.tabs.installation.reinstall.confirm.title": {
"message": "Ви впевнені, що хочете перевстановити цей профіль?"
@@ -366,7 +303,7 @@
"message": "Відновити профіль?"
},
"instance.settings.tabs.installation.repair.in-progress": {
"message": "Лагодження в процесі"
"message": "Відновлення в процессі"
},
"instance.settings.tabs.installation.reset-selections": {
"message": "Скинути до поточного"
@@ -384,7 +321,7 @@
"message": "перевстановити"
},
"instance.settings.tabs.installation.tooltip.action.repair": {
"message": "полагодити"
"message": "виправити"
},
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
"message": "Неможливо {action} під час встановлення"

View File

@@ -65,69 +65,6 @@
"app.update.reload-to-update": {
"message": "重新启动以安装更新"
},
"friends.action.add-friend": {
"message": "加一好友"
},
"friends.action.view-friend-requests": {
"message": "{count}好友请求"
},
"friends.add-friend.submit": {
"message": "发送好友请求"
},
"friends.add-friend.title": {
"message": "添加好友"
},
"friends.add-friend.username.description": {
"message": "可能和他们的 Minecraft 用户名不一样!"
},
"friends.add-friend.username.placeholder": {
"message": "输入Modrinth用户名……"
},
"friends.add-friend.username.title": {
"message": "你朋友的 Modrinth 用户名是什么?"
},
"friends.add-friends-to-share": {
"message": "<link>添加好友</link>查看他们在玩什么!"
},
"friends.friend.cancel-request": {
"message": "取消请求"
},
"friends.friend.remove-friend": {
"message": "删除好友"
},
"friends.friend.request-sent": {
"message": "已发送好友请求"
},
"friends.friend.view-profile": {
"message": "查看个人资料"
},
"friends.heading": {
"message": "好友"
},
"friends.heading.active": {
"message": "活跃的"
},
"friends.heading.offline": {
"message": "离线"
},
"friends.heading.online": {
"message": "在线"
},
"friends.heading.pending": {
"message": "待确认"
},
"friends.no-friends-match": {
"message": "没有匹配“{query}”的朋友"
},
"friends.search-friends-placeholder": {
"message": "搜索好友……"
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "请登录你的 <link>Modrinth账号</link> 添加好友并查看他们在玩什么!"
},
"instance.add-server.add-and-play": {
"message": "添加并游玩"
},

View File

@@ -36,7 +36,7 @@
"message": "變更紀錄"
},
"app.update-toast.download": {
"message": "下載 ({size})"
"message": "下載{size}"
},
"app.update-toast.downloading": {
"message": "下載中..."
@@ -60,74 +60,11 @@
"message": "下載更新"
},
"app.update.downloading-update": {
"message": "正在下載更新 ({percent}%)"
"message": "正在下載更新{percent}%"
},
"app.update.reload-to-update": {
"message": "重新載入即可安裝更新"
},
"friends.action.add-friend": {
"message": "新增好友"
},
"friends.action.view-friend-requests": {
"message": "{count} 個好友請求"
},
"friends.add-friend.submit": {
"message": "傳送好友請求"
},
"friends.add-friend.title": {
"message": "新增好友"
},
"friends.add-friend.username.description": {
"message": "這可能與對方的 Minecraft 使用者名稱不同!"
},
"friends.add-friend.username.placeholder": {
"message": "輸入 Modrinth 使用者名稱..."
},
"friends.add-friend.username.title": {
"message": "你好友的 Modrinth 使用者名稱是什麼?"
},
"friends.add-friends-to-share": {
"message": "<link>新增好友</link>即可查看對方正在玩什麼!"
},
"friends.friend.cancel-request": {
"message": "取消請求"
},
"friends.friend.remove-friend": {
"message": "移除好友"
},
"friends.friend.request-sent": {
"message": "好友請求已送出"
},
"friends.friend.view-profile": {
"message": "查看個人檔案"
},
"friends.heading": {
"message": "好友"
},
"friends.heading.active": {
"message": "活躍中"
},
"friends.heading.offline": {
"message": "離線"
},
"friends.heading.online": {
"message": "線上"
},
"friends.heading.pending": {
"message": "待處理"
},
"friends.no-friends-match": {
"message": "沒有符合「{query}」的好友"
},
"friends.search-friends-placeholder": {
"message": "搜尋好友..."
},
"friends.section.heading": {
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>登入 Modrinth 帳號</link>即可新增好友並查看對方正在玩什麼!"
},
"instance.add-server.add-and-play": {
"message": "新增並遊玩"
},

View File

@@ -2,7 +2,6 @@ import 'floating-vue/dist/style.css'
import * as Sentry from '@sentry/vue'
import { VueScanPlugin } from '@taijased/vue-render-tracker'
import { VueQueryPlugin } from '@tanstack/vue-query'
import { createPlugin } from '@vintl/vintl/plugin'
import FloatingVue from 'floating-vue'
import { createPinia } from 'pinia'
@@ -46,7 +45,6 @@ Sentry.init({
tracesSampleRate: 0.1,
})
app.use(VueQueryPlugin)
app.use(vueScan)
app.use(router)
app.use(pinia)

View File

@@ -427,7 +427,7 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
<div v-else class="flex items-center justify-center min-h-[50vh] pt-[25%]">
<div
class="bg-bg-raised card-shadow rounded-lg p-7 flex flex-col gap-5 shadow-md relative max-w-xl w-full mx-auto"
class="bg-bg-raised rounded-lg p-7 flex flex-col gap-5 shadow-md relative max-w-xl w-full mx-auto"
>
<img
:src="ExcitedRinthbot"

View File

@@ -67,7 +67,6 @@
direction="vertical"
:item-size="20"
key-field="id"
buffer="200"
>
<div class="user no-wrap">
<span :style="{ color: item.prefixColor, 'font-weight': item.weight }">{{
@@ -509,7 +508,7 @@ onUnmounted(() => {
background-color: var(--color-accent-contrast);
color: var(--color-contrast);
border-radius: var(--radius-lg);
padding-top: 1.5rem;
padding: 1.5rem;
overflow-x: auto; /* Enables horizontal scrolling */
overflow-y: hidden; /* Disables vertical scrolling on this wrapper */
white-space: nowrap; /* Keeps content on a single line */
@@ -558,10 +557,9 @@ onUnmounted(() => {
.user {
height: 32%;
padding: 0 1.5rem;
padding: 0 12px;
display: flex;
align-items: center;
user-select: text;
}
</style>

View File

@@ -285,7 +285,6 @@ import type { Organization, Project, TeamMember, Version } from '@modrinth/utils
import { formatProjectType } from '@modrinth/utils'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { useStorage } from '@vueuse/core'
import dayjs from 'dayjs'
import type { ComputedRef } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue'
@@ -301,13 +300,11 @@ import {
get_organization_many,
get_project_many,
get_team_many,
get_version,
get_version_many,
} from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events.js'
import {
add_project_from_path,
get,
get_projects,
remove_project,
toggle_disable_project,
@@ -316,7 +313,6 @@ import {
} from '@/helpers/profile.js'
import type { CacheBehaviour, ContentFile, GameInstance } from '@/helpers/types'
import { highlightModInProfile } from '@/helpers/utils.js'
import { installVersionDependencies } from '@/store/install'
const { handleError } = injectNotificationManager()
@@ -535,13 +531,7 @@ const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
return options
})
const selectedFilters = useStorage<string[]>(
`${props.instance.name}-mod-selected-filters`,
[],
sessionStorage,
{ mergeDefaults: true },
)
const selectedFilters = ref<string[]>([])
const filteredProjects = computed(() => {
const updatesFilter = selectedFilters.value.includes('updates')
const disabledFilter = selectedFilters.value.includes('disabled')
@@ -630,15 +620,10 @@ const sortProjects = (filter: string) => {
const updateAll = async () => {
const setProjects = []
const outdatedProjects = []
for (const [i, project] of projects.value.entries()) {
if (project.outdated) {
project.updating = true
setProjects.push(i)
if (project.updateVersion) {
outdatedProjects.push(project.updateVersion)
}
}
}
@@ -654,21 +639,6 @@ const updateAll = async () => {
projects.value[index].updateVersion = undefined
}
}
if (outdatedProjects.length > 0) {
const profile = await get(props.instance.path).catch(handleError)
if (profile) {
for (const versionId of outdatedProjects) {
const versionData = await get_version(versionId, 'must_revalidate').catch(handleError)
if (versionData) {
await installVersionDependencies(profile, versionData).catch(handleError)
}
}
}
}
for (const project of setProjects) {
projects.value[project].updating = false
}
@@ -685,19 +655,6 @@ const updateProject = async (mod: ProjectListEntry) => {
mod.updating = true
await new Promise((resolve) => setTimeout(resolve, 0))
mod.path = await update_project(props.instance.path, mod.path).catch(handleError)
if (mod.updateVersion) {
const versionData = await get_version(mod.updateVersion, 'must_revalidate').catch(handleError)
if (versionData) {
const profile = await get(props.instance.path).catch(handleError)
if (profile) {
await installVersionDependencies(profile, versionData).catch(handleError)
}
}
}
mod.updating = false
mod.outdated = false

View File

@@ -1,4 +1,3 @@
import { ServersManagePageIndex } from '@modrinth/ui'
import { createRouter, createWebHistory } from 'vue-router'
import * as Pages from '@/pages'
@@ -28,14 +27,6 @@ export default new createRouter({
breadcrumb: [{ name: 'Worlds' }],
},
},
{
path: '/servers/manage/',
name: 'Servers',
component: ServersManagePageIndex,
meta: {
breadcrumb: [{ name: 'Servers' }],
},
},
{
path: '/browse/:projectType',
name: 'Discover content',

View File

@@ -42,24 +42,11 @@ export const useInstall = defineStore('installStore', {
})
export const findPreferredVersion = (versions, project, instance) => {
// When `project` is passed in from this stack trace:
// - `installVersionDependencies`
// - `install.js/install` - `installVersionDependencies` call
//
// ..then `project` is actually a `Dependency` struct of a cached `Version`.
// `Dependency` does not have a `project_type` field,
// so we default it to `mod`.
//
// If we don't default here, then this `.find` will ignore version/instance
// loader mismatches, and you'll end up e.g. installing NeoForge mods for a
// Fabric instance.
const projectType = project.project_type ?? 'mod'
// If we can find a version using strictly the instance loader then prefer that
let version = versions.find(
(v) =>
v.game_versions.includes(instance.game_version) &&
(projectType === 'mod' ? v.loaders.includes(instance.loader) : true),
(project.project_type === 'mod' ? v.loaders.includes(instance.loader) : true),
)
if (!version) {

View File

@@ -5,7 +5,6 @@ export const DEFAULT_FEATURE_FLAGS = {
page_path: false,
worlds_tab: false,
worlds_in_home: true,
servers_in_app: false,
}
export const THEME_OPTIONS = ['dark', 'light', 'oled', 'system'] as const

View File

@@ -15,7 +15,6 @@ fn main() {
"offline_login",
"elyby_login",
"elyby_auth_authenticate",
"check_reachable",
"login",
"remove_user",
"get_default_user",

View File

@@ -11,7 +11,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
offline_login,
elyby_login,
elyby_auth_authenticate,
check_reachable,
login,
remove_user,
get_default_user,
@@ -80,13 +79,6 @@ pub async fn elyby_auth_authenticate(
Ok(text)
}
/// Checks if the authentication servers are reachable.
#[tauri::command]
pub async fn check_reachable() -> Result<()> {
minecraft_auth::check_reachable().await?;
Ok(())
}
/// Authenticate a user with Hydra - part 1
/// This begins the authentication flow quasi-synchronously, returning a URL to visit (that the user will sign in at)
#[tauri::command]

View File

@@ -2,7 +2,6 @@
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
#![recursion_limit = "256"]
use native_dialog::{DialogBuilder, MessageLevel};
use std::env;

View File

@@ -48,7 +48,7 @@
]
},
"productName": "AstralRinth App",
"version": "0.10.2101",
"version": "0.10.1601",
"mainBinaryName": "AstralRinth App",
"identifier": "AstralRinthApp",
"plugins": {

View File

@@ -703,8 +703,8 @@ components:
description: All gallery images attached to the project
example:
[
'https://cdn.modrinth.com/data/AABBCCDD/images/009b7d8d6e8bf04968a29421117c59b3efe2351a.png',
'https://cdn.modrinth.com/data/AABBCCDD/images/c21776867afb6046fdc3c21dbcf5cc50ae27a236.png',
https://cdn.modrinth.com/data/AABBCCDD/images/009b7d8d6e8bf04968a29421117c59b3efe2351a.png,
https://cdn.modrinth.com/data/AABBCCDD/images/c21776867afb6046fdc3c21dbcf5cc50ae27a236.png,
]
items:
type: string

View File

@@ -1,12 +1,12 @@
import { pathToFileURL } from 'node:url'
import { match as matchLocale } from '@formatjs/intl-localematcher'
import { GenericModrinthClient, type Labrinth } from '@modrinth/api-client'
import serverSidedVue from '@vitejs/plugin-vue'
import { consola } from 'consola'
import { promises as fs } from 'fs'
import { globIterate } from 'glob'
import { defineNuxtConfig } from 'nuxt/config'
import { $fetch } from 'ofetch'
import { basename, relative, resolve } from 'pathe'
import svgLoader from 'vite-svg-loader'
@@ -133,7 +133,20 @@ export default defineNuxtConfig({
// 30 minutes
const TTL = 30 * 60 * 1000
let state: Partial<Labrinth.State.GeneratedState & Record<string, any>> = {}
let state: {
lastGenerated?: string
apiUrl?: string
categories?: any[]
loaders?: any[]
gameVersions?: any[]
donationPlatforms?: any[]
reportTypes?: any[]
homePageProjects?: any[]
homePageSearch?: any[]
homePageNotifs?: any[]
products?: any[]
errors?: number[]
} = {}
try {
state = JSON.parse(await fs.readFile('./src/generated/state.json', 'utf8'))
@@ -159,19 +172,67 @@ export default defineNuxtConfig({
return
}
const client = new GenericModrinthClient({
labrinthBaseUrl: API_URL.replace('/v2/', ''),
userAgent: 'Knossos generator (support@modrinth.com)',
})
const generatedState = await client.labrinth.state.build()
state.lastGenerated = new Date().toISOString()
state.apiUrl = API_URL
state = {
...state,
...generatedState,
const headers = {
headers: {
'user-agent': 'Knossos generator (support@modrinth.com)',
},
}
const caughtErrorCodes = new Set<number>()
function handleFetchError(err: any, defaultValue: any) {
console.error('Error generating state: ', err)
caughtErrorCodes.add(err.status)
return defaultValue
}
const [
categories,
loaders,
gameVersions,
donationPlatforms,
reportTypes,
homePageProjects,
homePageSearch,
homePageNotifs,
products,
] = await Promise.all([
$fetch(`${API_URL}tag/category`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}tag/loader`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}tag/game_version`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}tag/donation_platform`, headers).catch((err) =>
handleFetchError(err, []),
),
$fetch(`${API_URL}tag/report_type`, headers).catch((err) => handleFetchError(err, [])),
$fetch(`${API_URL}projects_random?count=60`, headers).catch((err) =>
handleFetchError(err, []),
),
$fetch(`${API_URL}search?limit=3&query=leave&index=relevance`, headers).catch((err) =>
handleFetchError(err, {}),
),
$fetch(`${API_URL}search?limit=3&query=&index=updated`, headers).catch((err) =>
handleFetchError(err, {}),
),
$fetch(`${API_URL.replace('/v2/', '/_internal/')}billing/products`, headers).catch((err) =>
handleFetchError(err, []),
),
])
state.categories = categories
state.loaders = loaders
state.gameVersions = gameVersions
state.donationPlatforms = donationPlatforms
state.reportTypes = reportTypes
state.homePageProjects = homePageProjects
state.homePageSearch = homePageSearch
state.homePageNotifs = homePageNotifs
state.products = products
state.errors = [...caughtErrorCodes]
await fs.writeFile('./src/generated/state.json', JSON.stringify(state))
console.log('Tags generated!')
@@ -414,12 +475,6 @@ export default defineNuxtConfig({
'Critical-CH': 'Sec-CH-Prefers-Color-Scheme',
},
},
'/dashboard/revenue/withdraw': {
redirect: {
to: '/dashboard/revenue',
statusCode: 410,
},
},
'/email/**': {
redirect: '/_internal/templates/email/**',
},

View File

@@ -37,14 +37,12 @@
"@formatjs/intl-localematcher": "^0.5.4",
"@intercom/messenger-js-sdk": "^0.0.14",
"@ltd/j-toml": "^1.38.0",
"@modrinth/api-client": "workspace:*",
"@modrinth/assets": "workspace:*",
"@modrinth/blog": "workspace:*",
"@modrinth/moderation": "workspace:*",
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
"@pinia/nuxt": "^0.5.1",
"@tanstack/vue-query": "^5.90.7",
"@types/three": "^0.172.0",
"@vintl/vintl": "^4.4.1",
"@vitejs/plugin-vue": "^5.0.4",
@@ -58,6 +56,7 @@
"floating-vue": "^5.2.2",
"fuse.js": "^6.6.2",
"highlight.js": "^11.7.0",
"iso-3166-1": "^2.1.1",
"js-yaml": "^4.1.0",
"jszip": "^3.10.1",
"markdown-it": "14.1.0",

View File

@@ -6,21 +6,31 @@
</NuxtLayout>
</template>
<script setup lang="ts">
import { NotificationPanel, provideModrinthClient, provideNotificationManager } from '@modrinth/ui'
import { NotificationPanel, provideNotificationManager } from '@modrinth/ui'
import { provideApi } from '@modrinth/ui/src/providers/api.ts'
import { RestModrinthApi } from '@modrinth/utils'
import ModrinthLoadingIndicator from '~/components/ui/modrinth-loading-indicator.ts'
import { createModrinthClient } from '~/helpers/api.ts'
import { FrontendNotificationManager } from '~/providers/frontend-notifications.ts'
const auth = await useAuth()
const config = useRuntimeConfig()
import { FrontendNotificationManager } from './providers/frontend-notifications.ts'
provideNotificationManager(new FrontendNotificationManager())
const client = createModrinthClient(auth, {
apiBaseUrl: config.public.apiBaseUrl.replace('/v2/', '/'),
archonBaseUrl: config.public.pyroBaseUrl.replace('/v2/', '/'),
rateLimitKey: config.rateLimitKey,
})
provideModrinthClient(client)
provideApi(
new RestModrinthApi((url: string, options?: object) => {
const match = url.match(/^\/v(\d+)\/(.+)$/)
if (match) {
const apiVersion = Number(match[1])
const path = match[2]
return useBaseFetch(path, {
...options,
apiVersion,
}) as Promise<Response>
} else {
throw new Error('Invalid format')
}
}),
)
</script>

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -1290,7 +1290,3 @@ svg.inline-svg {
}
}
}
.card-shadow {
box-shadow: var(--shadow-card);
}

View File

@@ -37,7 +37,6 @@ html {
--icon-32: 2rem;
interpolate-size: allow-keywords;
scrollbar-gutter: stable;
}
.light-mode {
@@ -90,7 +89,7 @@ html {
--color-hr: var(--color-text);
--color-table-border: #dfe2e5;
--color-table-alternate-row: #f0f1f2;
--color-table-alternate-row: #f2f4f7;
--landing-maze-bg: url('https://cdn.modrinth.com/landing-new/landing-light.webp');
--landing-maze-gradient-bg: url('https://cdn.modrinth.com/landing-new/landing-lower-light.webp');

View File

@@ -53,15 +53,12 @@
</svg>
</template>
<script setup lang="ts">
<script setup>
const loading = useLoading()
const config = useRuntimeConfig()
const flags = useFeatureFlags()
const api = computed(() => {
if (flags.value.demoMode) return 'prod'
const apiUrl = config.public.apiBaseUrl
if (apiUrl.startsWith('https://api.modrinth.com')) {
return 'prod'

View File

@@ -0,0 +1,151 @@
<template>
<div
class="checkbox-outer button-within"
:class="{ disabled, checked: modelValue }"
role="presentation"
@click="toggle"
>
<button
class="checkbox"
role="checkbox"
:disabled="disabled"
:class="{ checked: modelValue, collapsing: collapsingToggleStyle }"
:aria-label="description ?? label"
:aria-checked="modelValue"
>
<CheckIcon v-if="modelValue && !collapsingToggleStyle" aria-hidden="true" />
<DropdownIcon v-else-if="collapsingToggleStyle" aria-hidden="true" />
</button>
<!-- aria-hidden is set so screenreaders only use the <button>'s aria-label -->
<p v-if="label" aria-hidden="true">
{{ label }}
</p>
<slot v-else />
</div>
</template>
<script>
import { CheckIcon, DropdownIcon } from '@modrinth/assets'
export default {
components: {
CheckIcon,
DropdownIcon,
},
props: {
label: {
type: String,
required: false,
default: '',
},
disabled: {
type: Boolean,
required: false,
default: false,
},
description: {
type: String,
required: false,
default: null,
},
modelValue: Boolean,
clickEvent: {
type: Function,
required: false,
default: () => {},
},
collapsingToggleStyle: {
type: Boolean,
required: false,
default: false,
},
},
emits: ['update:modelValue'],
methods: {
toggle() {
if (!this.disabled) {
this.$emit('update:modelValue', !this.modelValue)
}
},
},
}
</script>
<style lang="scss" scoped>
.checkbox-outer {
display: flex;
align-items: center;
cursor: pointer;
p {
user-select: none;
padding: 0.2rem 0;
margin: 0;
}
&.disabled {
cursor: not-allowed;
}
&.checked {
outline: 2px solid transparent;
outline-offset: 4px;
border-radius: 0.25rem;
}
}
.checkbox {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
min-width: 1rem;
min-height: 1rem;
padding: 0;
margin: 0 0.5rem 0 0;
color: var(--color-button-text);
background-color: var(--color-button-bg);
border-radius: var(--size-rounded-control);
box-shadow:
var(--shadow-inset-sm),
0 0 0 0 transparent;
&.checked {
background-color: var(--color-brand);
}
svg {
color: var(--color-accent-contrast, var(--color-brand-inverted));
stroke-width: 0.2rem;
height: 0.8rem;
width: 0.8rem;
flex-shrink: 0;
}
&.collapsing {
background-color: transparent !important;
box-shadow: none;
svg {
color: inherit;
height: 1rem;
width: 1rem;
transition: transform 0.25s ease-in-out;
}
&.checked {
svg {
transform: rotate(180deg);
}
}
}
&:disabled {
box-shadow: none;
cursor: not-allowed;
}
}
</style>

View File

@@ -90,7 +90,7 @@ defineProps({
},
})
const tags = useGeneratedState()
const tags = useTags()
</script>
<style lang="scss" scoped>
.environment {

View File

@@ -1,155 +1,32 @@
<template>
<nav :aria-label="ariaLabel" class="w-full">
<ul
class="card-shadow m-0 flex list-none flex-col items-start gap-1.5 rounded-2xl bg-bg-raised p-4"
>
<slot v-if="hasSlotContent" />
<template v-else>
<li v-for="(item, idx) in filteredItems" :key="getKey(item, idx)" class="contents">
<hr v-if="isSeparator(item)" class="my-1 w-full border-t border-solid" />
<div
v-else-if="isHeading(item)"
class="px-4 pb-1 pt-2 text-xs font-bold uppercase tracking-wide text-secondary"
>
{{ item.label }}
</div>
<NuxtLink
v-else-if="item.link ?? item.to"
:to="(item.link ?? item.to) as string"
class="nav-item inline-flex w-full cursor-pointer items-center gap-2 text-nowrap rounded-xl border-none bg-transparent px-4 py-2.5 text-left text-base font-semibold leading-tight text-button-text transition-all hover:bg-button-bg hover:text-contrast active:scale-[0.97]"
:class="{ 'is-active': isActive(item as NavStackLinkItem) }"
>
<component
:is="item.icon"
v-if="item.icon"
aria-hidden="true"
class="h-5 w-5 shrink-0"
/>
<span class="text-contrast">{{ item.label }}</span>
<span
v-if="item.badge != null"
class="rounded-full bg-brand-highlight px-2 text-sm font-bold text-brand"
>
{{ String(item.badge) }}
</span>
<span v-if="item.chevron" class="ml-auto"><ChevronRightIcon /></span>
</NuxtLink>
<button
v-else-if="item.action"
class="nav-item inline-flex w-full cursor-pointer items-center gap-2 text-nowrap rounded-xl border-none bg-transparent px-4 py-2.5 text-left text-base font-semibold leading-tight text-button-text transition-all hover:bg-button-bg hover:text-contrast active:scale-[0.97]"
:class="{ 'danger-button': item.danger }"
@click="item.action"
>
<component
:is="item.icon"
v-if="item.icon"
aria-hidden="true"
class="h-5 w-5 shrink-0"
/>
<span class="text-contrast">{{ item.label }}</span>
<span
v-if="item.badge != null"
class="rounded-full bg-brand-highlight px-2 text-sm font-bold text-brand"
>
{{ String(item.badge) }}
</span>
</button>
<span v-else>You frog. 🐸</span>
</li>
</template>
<nav>
<ul>
<slot />
</ul>
</nav>
</template>
<script setup lang="ts">
import { ChevronRightIcon } from '@modrinth/assets'
import { type Component, computed, useSlots } from 'vue'
type NavStackBaseItem = {
label: string
icon?: Component | string
badge?: string | number | null
chevron?: boolean
danger?: boolean
}
type NavStackLinkItem = NavStackBaseItem & {
type?: 'item'
link?: string | null
to?: string | null
action?: (() => void) | null
matchNested?: boolean
}
type NavStackSeparator = { type: 'separator' }
type NavStackHeading = { type: 'heading'; label: string }
export type NavStackEntry = (NavStackLinkItem | NavStackSeparator | NavStackHeading) & {
shown?: boolean
}
const props = defineProps<{
items?: NavStackEntry[]
ariaLabel?: string
}>()
const ariaLabel = computed(() => props.ariaLabel ?? 'Section navigation')
const route = useRoute()
const slots = useSlots()
const hasSlotContent = computed(() => {
const content = slots.default?.()
return !!(content && content.length)
})
function isSeparator(item: NavStackEntry): item is NavStackSeparator {
return (item as any).type === 'separator'
}
function isHeading(item: NavStackEntry): item is NavStackHeading {
return (item as any).type === 'heading'
}
function getKey(item: NavStackEntry, idx: number) {
if (isSeparator(item)) return `sep-${idx}`
if (isHeading(item)) return `head-${item.label}-${idx}`
const link = (item as NavStackLinkItem).link ?? (item as NavStackLinkItem).to
return link ? `link-${link}` : `action-${(item as NavStackLinkItem).label}-${idx}`
}
function isActive(item: NavStackLinkItem): boolean {
const link = item.link ?? item.to
if (!link) return false
const currentPath = route.path
if (item.matchNested) {
return currentPath.startsWith(link)
}
return currentPath === link
}
const filteredItems = computed(() => props.items?.filter((x) => x.shown === undefined || x.shown))
<script>
export default {}
</script>
<style lang="scss" scoped>
ul {
display: flex;
flex-direction: column;
grid-gap: var(--spacing-card-xs);
flex-wrap: wrap;
list-style-type: none;
margin: 0;
padding: 0;
> :first-child {
margin-top: 0;
}
}
li {
display: unset;
text-align: unset;
}
.router-link-exact-active.nav-item,
.nav-item.is-active {
background: var(--color-button-bg-selected);
color: var(--color-button-text-selected);
}
.router-link-exact-active.nav-item .text-contrast,
.nav-item.is-active .text-contrast {
color: var(--color-button-text-selected);
}
</style>

View File

@@ -0,0 +1,64 @@
<template>
<NuxtLink v-if="link !== null" :to="link" class="nav-item">
<slot />
<span>{{ label }}</span>
<span v-if="badge" class="rounded-full bg-brand-highlight px-2 text-sm font-bold text-brand">{{
badge
}}</span>
<span v-if="chevron" class="ml-auto"><ChevronRightIcon /></span>
</NuxtLink>
<button v-else-if="action" class="nav-item" :class="{ 'danger-button': danger }" @click="action">
<slot />
<span>{{ label }}</span>
<span v-if="badge" class="rounded-full bg-brand-highlight px-2 text-sm font-bold text-brand">{{
badge
}}</span>
</button>
<span v-else>i forgor 💀</span>
</template>
<script>
import { ChevronRightIcon } from '@modrinth/assets'
export default {
components: {
ChevronRightIcon,
},
props: {
link: {
default: null,
type: String,
},
action: {
default: null,
type: Function,
},
label: {
required: true,
type: String,
},
badge: {
default: null,
type: String,
},
chevron: {
default: false,
type: Boolean,
},
danger: {
default: false,
type: Boolean,
},
},
}
</script>
<style lang="scss" scoped>
.nav-item {
@apply flex w-full cursor-pointer items-center gap-2 text-nowrap rounded-xl border-none bg-transparent px-4 py-2 text-left font-semibold text-button-text transition-all hover:bg-button-bg hover:text-contrast active:scale-[0.97];
}
.router-link-exact-active.nav-item {
@apply bg-button-bgSelected text-button-textSelected;
}
</style>

View File

@@ -376,7 +376,7 @@ const props = defineProps({
})
const flags = useFeatureFlags()
const tags = useGeneratedState()
const tags = useTags()
const type = computed(() =>
!props.notification.body || props.notification.body.type === 'legacy_markdown'

View File

@@ -152,7 +152,7 @@ const onSubmitHandler = () => {
border-radius: var(--radius-md);
overflow: hidden;
margin-top: var(--gap-md);
border: 1px solid var(--color-divider);
border: 1px solid var(--color-button-bg);
background-color: var(--color-raised-bg);
.table-row {

View File

@@ -212,7 +212,7 @@ export default {
},
},
setup() {
const tags = useGeneratedState()
const tags = useTags()
const formatRelativeTime = useRelativeTime()
return { tags, formatRelativeTime }

View File

@@ -1,264 +0,0 @@
<template>
<NewModal ref="modal">
<template #title>
<span class="text-lg font-extrabold text-contrast">Batch credit</span>
</template>
<div class="flex w-[500px] max-w-[90vw] flex-col gap-6">
<div class="flex flex-col gap-2">
<label class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Type </span>
<span>Select target to credit.</span>
</label>
<Combobox
v-model="mode"
:options="modeOptions"
placeholder="Select type"
class="max-w-[8rem]"
/>
</div>
<div class="flex flex-col gap-2">
<label for="days" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Days to credit </span>
</label>
<input
id="days"
v-model.number="days"
class="w-32"
type="number"
min="1"
autocomplete="off"
/>
</div>
<div v-if="mode === 'nodes'" class="flex flex-col gap-3">
<div class="flex flex-col gap-2">
<label for="node-input" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Node hostnames </span>
</label>
<div class="flex items-center gap-2">
<input
id="node-input"
v-model="nodeInput"
class="w-32"
type="text"
autocomplete="off"
/>
<ButtonStyled color="blue" color-fill="text">
<button class="shrink-0" @click="addNode">
<PlusIcon />
Add
</button>
</ButtonStyled>
</div>
<div v-if="selectedNodes.length" class="mt-1 flex flex-wrap gap-2">
<TagItem v-for="h in selectedNodes" :key="`node-${h}`" :action="() => removeNode(h)">
<XIcon />
{{ h }}
</TagItem>
</div>
</div>
</div>
<div v-else class="flex flex-col gap-3">
<div class="flex flex-col gap-2">
<label for="region-select" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Region </span>
<span>This will credit all active servers in the region.</span>
</label>
<Combobox
v-model="selectedRegion"
:options="regions"
placeholder="Select region"
class="max-w-[24rem]"
/>
</div>
</div>
<div class="between flex items-center gap-4">
<label for="send-email-batch" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Send email </span>
</label>
<Toggle id="send-email-batch" v-model="sendEmail" />
</div>
<div v-if="sendEmail" class="flex flex-col gap-2">
<label for="message-batch" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-contrast"> Customize Email </span>
<span>
Unless a particularly bad or out of the ordinary event happened, keep this to the
default
</span>
</label>
<div
class="text-muted flex flex-col gap-2 rounded-lg border border-divider bg-button-bg p-4"
>
<span>Hi {user.name},</span>
<div class="textarea-wrapper">
<textarea
id="message-batch"
v-model="message"
rows="3"
class="w-full overflow-hidden !bg-surface-3"
/>
</div>
<span>
To make up for it, we've added {{ days }} day{{ pluralize(days) }} to your Modrinth
Servers subscription.
</span>
<span>
Your next charge was scheduled for {credit.previous_due} and will now be on
{credit.next_due}.
</span>
</div>
</div>
<div class="flex gap-2">
<ButtonStyled color="brand">
<button :disabled="applyDisabled" @click="apply">
<CheckIcon aria-hidden="true" />
Apply credits
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="modal?.hide?.()">
<XIcon aria-hidden="true" />
Cancel
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
</template>
<script setup lang="ts">
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
Combobox,
injectNotificationManager,
NewModal,
TagItem,
Toggle,
} from '@modrinth/ui'
import { DEFAULT_CREDIT_EMAIL_MESSAGE } from '@modrinth/utils/utils.ts'
import { computed, ref } from 'vue'
import { useBaseFetch } from '#imports'
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
const { addNotification } = injectNotificationManager()
const modal = ref<InstanceType<typeof NewModal>>()
const days = ref(1)
const sendEmail = ref(true)
const message = ref('')
const modeOptions = [
{ value: 'nodes', label: 'Nodes' },
{ value: 'region', label: 'Region' },
]
const mode = ref<string>('nodes')
const nodeInput = ref('')
const selectedNodes = ref<string[]>([])
type RegionOpt = { value: string; label: string }
const regions = ref<RegionOpt[]>([])
const selectedRegion = ref<string | null>(null)
const nodeHostnames = ref<string[]>([])
function show(event?: Event) {
void ensureOverview()
message.value = DEFAULT_CREDIT_EMAIL_MESSAGE
modal.value?.show(event)
}
function hide() {
modal.value?.hide()
}
function addNode() {
const v = nodeInput.value.trim()
if (!v) return
if (!nodeHostnames.value.includes(v)) {
addNotification({
title: 'Unknown node',
text: "This hostname doesn't exist",
type: 'error',
})
return
}
if (!selectedNodes.value.includes(v)) selectedNodes.value.push(v)
nodeInput.value = ''
}
function removeNode(v: string) {
selectedNodes.value = selectedNodes.value.filter((x) => x !== v)
}
const applyDisabled = computed(() => {
if (days.value < 1) return true
if (mode.value === 'nodes') return selectedNodes.value.length === 0
return !selectedRegion.value
})
async function ensureOverview() {
if (regions.value.length || nodeHostnames.value.length) return
try {
const data = await useServersFetch<any>('/nodes/overview', { version: 'internal' })
regions.value = (data.regions || []).map((r: any) => ({
value: r.key,
label: `${r.display_name} (${r.key})`,
}))
nodeHostnames.value = data.node_hostnames || []
if (!selectedRegion.value && regions.value.length) selectedRegion.value = regions.value[0].value
} catch (err) {
addNotification({ title: 'Failed to load nodes overview', text: String(err), type: 'error' })
}
}
async function apply() {
try {
const body =
mode.value === 'nodes'
? {
nodes: selectedNodes.value.slice(),
days: Math.max(1, Math.floor(days.value)),
send_email: sendEmail.value,
message: message.value?.trim() || DEFAULT_CREDIT_EMAIL_MESSAGE,
}
: {
region: selectedRegion.value!,
days: Math.max(1, Math.floor(days.value)),
send_email: sendEmail.value,
message: message.value?.trim() || DEFAULT_CREDIT_EMAIL_MESSAGE,
}
await useBaseFetch('billing/credit', {
method: 'POST',
body: JSON.stringify(body),
internal: true,
})
addNotification({ title: 'Credits applied', type: 'success' })
modal.value?.hide()
selectedNodes.value = []
nodeInput.value = ''
message.value = ''
} catch (err: any) {
addNotification({
title: 'Error applying credits',
text: err?.data?.description ?? String(err),
type: 'error',
})
}
}
function pluralize(n: number): string {
return n === 1 ? '' : 's'
}
defineExpose({
show,
hide,
})
</script>

View File

@@ -356,7 +356,7 @@ svg {
:deep(.apexcharts-yaxistooltip) {
background: var(--color-raised-bg) !important;
border-radius: var(--radius-sm) !important;
border: 1px solid var(--color-divider) !important;
border: 1px solid var(--color-button-bg) !important;
box-shadow: var(--shadow-floating) !important;
font-size: var(--font-size-nm) !important;
}
@@ -371,7 +371,7 @@ svg {
:deep(.apexcharts-xaxistooltip) {
background: var(--color-raised-bg) !important;
border-radius: var(--radius-sm) !important;
border: 1px solid var(--color-divider) !important;
border: 1px solid var(--color-button-bg) !important;
font-size: var(--font-size-nm) !important;
color: var(--color-base) !important;

View File

@@ -891,7 +891,7 @@ const defaultRanges: RangeObject[] = [
flex-direction: column;
background-color: var(--color-bg);
border-radius: var(--radius-sm);
border: 1px solid var(--color-divider);
border: 1px solid var(--color-button-bg);
gap: var(--gap-md);
padding: var(--gap-md);
margin-top: var(--gap-md);
@@ -920,7 +920,7 @@ const defaultRanges: RangeObject[] = [
width: 100%;
height: 1rem;
background-color: var(--color-raised-bg);
border: 1px solid var(--color-divider);
border: 1px solid var(--color-button-bg);
border-radius: var(--radius-sm);
overflow: hidden;

View File

@@ -159,10 +159,10 @@ defineExpose({
flex-direction: column;
gap: var(--gap-xs);
border: 1px solid var(--color-divider);
border: 1px solid var(--color-button-bg);
border-radius: var(--radius-md);
background-color: var(--color-raised-bg);
box-shadow: var(--shadow-card);
box-shadow: var(--shadow-floating);
color: var(--color-base);
font-size: var(--font-size-nm);
@@ -192,7 +192,7 @@ svg {
:deep(.apexcharts-yaxistooltip) {
background: var(--color-raised-bg) !important;
border-radius: var(--radius-sm) !important;
border: 1px solid var(--color-divider) !important;
border: 1px solid var(--color-button-bg) !important;
box-shadow: var(--shadow-floating) !important;
font-size: var(--font-size-nm) !important;
}

View File

@@ -1,570 +0,0 @@
<template>
<NewModal
ref="withdrawModal"
:closable="currentStage !== 'completion'"
:hide-header="currentStage === 'completion'"
:merge-header="currentStage === 'completion'"
:scrollable="true"
max-content-height="72vh"
:on-hide="onModalHide"
>
<template #title>
<div v-if="shouldShowTitle" class="flex flex-wrap items-center gap-1 text-secondary">
<template v-if="currentStage === 'tax-form'">
<span class="text-lg font-bold text-contrast sm:text-xl">{{
formatMessage(messages.taxFormStage)
}}</span>
</template>
<template v-else-if="currentStage === 'method-selection'">
<span class="text-lg font-bold text-contrast sm:text-xl">{{
formatMessage(messages.methodSelectionStage)
}}</span>
<ChevronRightIcon class="size-5 text-secondary" stroke-width="3" />
<span class="text-lg text-secondary sm:text-xl">{{
formatMessage(messages.detailsLabel)
}}</span>
</template>
<template v-else-if="isDetailsStage">
<button
class="active:scale-9 bg-transparent p-0 text-lg text-secondary transition-colors duration-200 hover:text-primary sm:text-xl"
@click="goToBreadcrumbStage('method-selection')"
>
{{ formatMessage(messages.methodSelectionStage) }}
</button>
<ChevronRightIcon class="size-5 text-secondary" stroke-width="3" />
<span class="text-lg font-bold text-contrast sm:text-xl">{{
formatMessage(messages.detailsLabel)
}}</span>
</template>
</div>
</template>
<div class="mx-auto w-full max-w-[496px] sm:mx-0 sm:min-w-[496px]">
<TaxFormStage
v-if="currentStage === 'tax-form'"
:balance="balance"
:on-show-tax-form="showTaxFormModal"
/>
<MethodSelectionStage
v-else-if="currentStage === 'method-selection'"
:on-show-tax-form="showTaxFormModal"
@close-modal="withdrawModal?.hide()"
/>
<TremendousDetailsStage v-else-if="currentStage === 'tremendous-details'" />
<MuralpayKycStage v-else-if="currentStage === 'muralpay-kyc'" />
<MuralpayDetailsStage v-else-if="currentStage === 'muralpay-details'" />
<LegacyPaypalDetailsStage v-else-if="currentStage === 'paypal-details'" />
<CompletionStage v-else-if="currentStage === 'completion'" />
<div v-else>Something went wrong</div>
</div>
<template #actions>
<div v-if="currentStage === 'completion'" class="mt-4 flex w-full gap-3">
<ButtonStyled class="flex-1">
<button class="w-full text-contrast" @click="handleClose">
{{ formatMessage(messages.closeButton) }}
</button>
</ButtonStyled>
<ButtonStyled class="flex-1">
<button class="w-full text-contrast" @click="handleViewTransactions">
{{ formatMessage(messages.transactionsButton) }}
</button>
</ButtonStyled>
</div>
<div v-else class="mt-4 flex flex-col justify-end gap-2 sm:flex-row">
<ButtonStyled type="outlined">
<button
class="!border-surface-5"
:disabled="leftButtonConfig.disabled"
@click="leftButtonConfig.handler"
>
<component :is="leftButtonConfig.icon" />
{{ leftButtonConfig.label }}
</button>
</ButtonStyled>
<ButtonStyled :color="rightButtonConfig.color">
<button :disabled="rightButtonConfig.disabled" @click="rightButtonConfig.handler">
<component
:is="rightButtonConfig.icon"
v-if="rightButtonConfig.iconPosition === 'before'"
:class="rightButtonConfig.iconClass"
/>
{{ rightButtonConfig.label }}
<component
:is="rightButtonConfig.icon"
v-if="rightButtonConfig.iconPosition === 'after'"
:class="rightButtonConfig.iconClass"
/>
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
<CreatorTaxFormModal
ref="taxFormModal"
close-button-text="Continue"
@success="onTaxFormSuccess"
@cancelled="onTaxFormCancelled"
/>
</template>
<script setup lang="ts">
import {
ArrowLeftRightIcon,
ChevronRightIcon,
FileTextIcon,
LeftArrowIcon,
RightArrowIcon,
SpinnerIcon,
XIcon,
} from '@modrinth/assets'
import { ButtonStyled, commonMessages, injectNotificationManager, NewModal } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'
import {
createWithdrawContext,
type PayoutMethod,
provideWithdrawContext,
TAX_THRESHOLD_ACTUAL,
type WithdrawStage,
} from '@/providers/creator-withdraw.ts'
import CreatorTaxFormModal from './CreatorTaxFormModal.vue'
import CompletionStage from './withdraw-stages/CompletionStage.vue'
import LegacyPaypalDetailsStage from './withdraw-stages/LegacyPaypalDetailsStage.vue'
import MethodSelectionStage from './withdraw-stages/MethodSelectionStage.vue'
import MuralpayDetailsStage from './withdraw-stages/MuralpayDetailsStage.vue'
import MuralpayKycStage from './withdraw-stages/MuralpayKycStage.vue'
import TaxFormStage from './withdraw-stages/TaxFormStage.vue'
import TremendousDetailsStage from './withdraw-stages/TremendousDetailsStage.vue'
type FormCompletionStatus = 'unknown' | 'unrequested' | 'unsigned' | 'tin-mismatch' | 'complete'
interface UserBalanceResponse {
available: number
withdrawn_lifetime: number
withdrawn_ytd: number
pending: number
dates: Record<string, number>
requested_form_type: string | null
form_completion_status: FormCompletionStatus | null
}
const props = defineProps<{
balance: UserBalanceResponse | null
preloadedPaymentData?: { country: string; methods: PayoutMethod[] } | null
}>()
const emit = defineEmits<{
(e: 'refresh-data' | 'hide'): void
}>()
const withdrawModal = useTemplateRef<InstanceType<typeof NewModal>>('withdrawModal')
const taxFormModal = ref<InstanceType<typeof CreatorTaxFormModal> | null>(null)
const isSubmitting = ref(false)
function show(preferred?: WithdrawStage) {
if (preferred) {
setStage(preferred, true)
withdrawModal.value?.show()
return
}
const firstStage = stages.value[0]
if (firstStage) {
setStage(firstStage, true)
}
withdrawModal.value?.show()
}
defineExpose({
show,
})
const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()
const withdrawContext = createWithdrawContext(
props.balance,
props.preloadedPaymentData || undefined,
)
provideWithdrawContext(withdrawContext)
const {
currentStage,
previousStep,
nextStep,
canProceed,
setStage,
withdrawData,
resetData,
stages,
submitWithdrawal,
restoreStateFromStorage,
clearSavedState,
} = withdrawContext
watch(
() => props.balance,
(newBalance) => {
if (newBalance) {
withdrawContext.balance.value = newBalance
}
},
{ deep: true },
)
onMounted(() => {
const route = useRoute()
const router = useRouter()
if (route.query.paypal_auth_return === 'true') {
const savedState = restoreStateFromStorage()
if (savedState?.data) {
withdrawData.value = { ...savedState.data }
nextTick(() => {
show(savedState.stage)
})
clearSavedState()
}
const query = { ...route.query }
delete query.paypal_auth_return
router.replace({ query })
}
})
const needsTaxForm = computed(() => {
if (!props.balance || currentStage.value !== 'tax-form') return false
const ytd = props.balance.withdrawn_ytd ?? 0
const available = props.balance.available ?? 0
const status = props.balance.form_completion_status
return status !== 'complete' && ytd + available >= 600
})
const remainingLimit = computed(() => {
if (!props.balance) return 0
const ytd = props.balance.withdrawn_ytd ?? 0
const raw = TAX_THRESHOLD_ACTUAL - ytd
if (raw <= 0) return 0
const cents = Math.floor(raw * 100)
return cents / 100
})
const leftButtonConfig = computed(() => {
if (previousStep.value) {
return {
icon: LeftArrowIcon,
label: formatMessage(commonMessages.backButton),
handler: () => setStage(previousStep.value, true),
disabled: isSubmitting.value,
}
}
return {
icon: XIcon,
label: formatMessage(commonMessages.cancelButton),
handler: () => withdrawModal.value?.hide(),
disabled: isSubmitting.value,
}
})
const rightButtonConfig = computed(() => {
const stage = currentStage.value
const isTaxFormStage = stage === 'tax-form'
const isDetailsStage =
stage === 'muralpay-details' || stage === 'tremendous-details' || stage === 'paypal-details'
if (isTaxFormStage && needsTaxForm.value && remainingLimit.value > 0) {
return {
icon: RightArrowIcon,
label: formatMessage(messages.continueWithLimit),
handler: continueWithLimit,
disabled: false,
color: 'standard' as const,
iconPosition: 'after' as const,
}
}
if (isTaxFormStage && needsTaxForm.value) {
return {
icon: FileTextIcon,
label: formatMessage(messages.completeTaxForm),
handler: showTaxFormModal,
disabled: false,
color: 'orange' as const,
iconPosition: 'before' as const,
}
}
if (isDetailsStage) {
return {
icon: isSubmitting.value ? SpinnerIcon : ArrowLeftRightIcon,
iconClass: isSubmitting.value ? 'animate-spin' : undefined,
label: formatMessage(messages.withdrawButton),
handler: handleWithdraw,
disabled: !canProceed.value || isSubmitting.value,
color: 'brand' as const,
iconPosition: 'before' as const,
}
}
return {
icon: RightArrowIcon,
label: formatMessage(commonMessages.nextButton),
handler: () => setStage(nextStep.value),
disabled: !canProceed.value,
color: 'standard' as const,
iconPosition: 'after' as const,
}
})
function continueWithLimit() {
withdrawData.value.tax.skipped = true
setStage(nextStep.value)
}
// TODO: God we need better errors from the backend (e.g error ids), this shit is insane
function getWithdrawalError(error: any): { title: string; text: string } {
const description = error?.data?.description?.toLowerCase() || ''
// Tax form error
if (description.includes('tax form')) {
return {
title: formatMessage(messages.errorTaxFormTitle),
text: formatMessage(messages.errorTaxFormText),
}
}
// Invalid crypto wallet address
if (
(description.includes('wallet') && description.includes('invalid')) ||
description.includes('wallet_address') ||
(description.includes('blockchain') && description.includes('invalid'))
) {
return {
title: formatMessage(messages.errorInvalidWalletTitle),
text: formatMessage(messages.errorInvalidWalletText),
}
}
// Invalid bank details
if (
(description.includes('bank') || description.includes('account')) &&
(description.includes('invalid') || description.includes('failed'))
) {
return {
title: formatMessage(messages.errorInvalidBankTitle),
text: formatMessage(messages.errorInvalidBankText),
}
}
// Invalid/fraudulent address
if (
description.includes('address') &&
(description.includes('invalid') ||
description.includes('verification') ||
description.includes('fraudulent'))
) {
return {
title: formatMessage(messages.errorInvalidAddressTitle),
text: formatMessage(messages.errorInvalidAddressText),
}
}
// Minimum amount not met
if (
description.includes('payoutminimumnotmeterror') ||
description.includes('minimum') ||
(description.includes('amount') && description.includes('less'))
) {
return {
title: formatMessage(messages.errorMinimumNotMetTitle),
text: formatMessage(messages.errorMinimumNotMetText),
}
}
// Generic fallback
return {
title: formatMessage(messages.errorGenericTitle),
text: formatMessage(messages.errorGenericText),
}
}
async function handleWithdraw() {
if (isSubmitting.value) return
try {
isSubmitting.value = true
await submitWithdrawal()
setStage('completion')
} catch (error) {
console.error('Withdrawal failed:', error)
const { title, text } = getWithdrawalError(error)
addNotification({
title,
text,
type: 'error',
})
} finally {
isSubmitting.value = false
}
}
const shouldShowTitle = computed(() => {
return currentStage.value !== 'completion'
})
const isDetailsStage = computed(() => {
const detailsStages: WithdrawStage[] = [
'tremendous-details',
'muralpay-kyc',
'muralpay-details',
'paypal-details',
]
const current = currentStage.value
return current ? detailsStages.includes(current) : false
})
function showTaxFormModal(e?: MouseEvent) {
withdrawModal.value?.hide()
taxFormModal.value?.startTaxForm(e ?? new MouseEvent('click'))
}
function onTaxFormSuccess() {
emit('refresh-data')
nextTick(() => {
show('method-selection')
})
}
function onTaxFormCancelled() {
show('tax-form')
}
function onModalHide() {
resetData()
emit('hide')
}
function goToBreadcrumbStage(stage: WithdrawStage) {
setStage(stage, true)
}
function handleClose() {
withdrawModal.value?.hide()
emit('refresh-data')
}
function handleViewTransactions() {
withdrawModal.value?.hide()
navigateTo('/dashboard/revenue/transfers')
}
const messages = defineMessages({
taxFormStage: {
id: 'dashboard.creator-withdraw-modal.stage.tax-form',
defaultMessage: 'Tax form',
},
methodSelectionStage: {
id: 'dashboard.creator-withdraw-modal.stage.method-selection',
defaultMessage: 'Method',
},
tremendousDetailsStage: {
id: 'dashboard.creator-withdraw-modal.stage.tremendous-details',
defaultMessage: 'Details',
},
muralpayKycStage: {
id: 'dashboard.creator-withdraw-modal.stage.muralpay-kyc',
defaultMessage: 'Verification',
},
muralpayDetailsStage: {
id: 'dashboard.creator-withdraw-modal.stage.muralpay-details',
defaultMessage: 'Account Details',
},
completionStage: {
id: 'dashboard.creator-withdraw-modal.stage.completion',
defaultMessage: 'Complete',
},
detailsLabel: {
id: 'dashboard.creator-withdraw-modal.details-label',
defaultMessage: 'Details',
},
completeTaxForm: {
id: 'dashboard.creator-withdraw-modal.complete-tax-form',
defaultMessage: 'Complete tax form',
},
continueWithLimit: {
id: 'dashboard.creator-withdraw-modal.continue-with-limit',
defaultMessage: 'Continue with limit',
},
withdrawButton: {
id: 'dashboard.creator-withdraw-modal.withdraw-button',
defaultMessage: 'Withdraw',
},
closeButton: {
id: 'dashboard.withdraw.completion.close-button',
defaultMessage: 'Close',
},
transactionsButton: {
id: 'dashboard.withdraw.completion.transactions-button',
defaultMessage: 'Transactions',
},
errorTaxFormTitle: {
id: 'dashboard.withdraw.error.tax-form.title',
defaultMessage: 'Please complete tax form',
},
errorTaxFormText: {
id: 'dashboard.withdraw.error.tax-form.text',
defaultMessage: 'You must complete a tax form to submit your withdrawal request.',
},
errorInvalidWalletTitle: {
id: 'dashboard.withdraw.error.invalid-wallet.title',
defaultMessage: 'Invalid wallet address',
},
errorInvalidWalletText: {
id: 'dashboard.withdraw.error.invalid-wallet.text',
defaultMessage:
'The crypto wallet address you provided is invalid. Please double-check and try again.',
},
errorInvalidBankTitle: {
id: 'dashboard.withdraw.error.invalid-bank.title',
defaultMessage: 'Invalid bank details',
},
errorInvalidBankText: {
id: 'dashboard.withdraw.error.invalid-bank.text',
defaultMessage:
'The bank account details you provided are invalid. Please verify your information.',
},
errorInvalidAddressTitle: {
id: 'dashboard.withdraw.error.invalid-address.title',
defaultMessage: 'Address verification failed',
},
errorInvalidAddressText: {
id: 'dashboard.withdraw.error.invalid-address.text',
defaultMessage:
'The address you provided could not be verified. Please check your address details.',
},
errorMinimumNotMetTitle: {
id: 'dashboard.withdraw.error.minimum-not-met.title',
defaultMessage: 'Amount too low',
},
errorMinimumNotMetText: {
id: 'dashboard.withdraw.error.minimum-not-met.text',
defaultMessage:
"The withdrawal amount (after fees) doesn't meet the minimum requirement. Please increase your withdrawal amount.",
},
errorGenericTitle: {
id: 'dashboard.withdraw.error.generic.title',
defaultMessage: 'Unable to withdraw',
},
errorGenericText: {
id: 'dashboard.withdraw.error.generic.text',
defaultMessage:
'We were unable to submit your withdrawal request, please check your details or contact support.',
},
})
</script>

View File

@@ -1,158 +0,0 @@
<template>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<div class="relative flex-1">
<input
ref="amountInput"
:value="modelValue"
type="number"
step="0.01"
:min="minAmount"
:max="safeMaxAmount"
:disabled="isDisabled"
:placeholder="formatMessage(formFieldPlaceholders.amountPlaceholder)"
class="w-full rounded-[14px] bg-surface-4 py-2.5 pl-4 pr-4 text-contrast placeholder:text-secondary"
@input="handleInput"
/>
</div>
<Combobox
v-if="showCurrencySelector"
:model-value="selectedCurrency"
:options="currencyOptions"
class="w-min"
@update:model-value="$emit('update:selectedCurrency', $event)"
>
<template v-for="option in currencyOptions" :key="option.value" #[`option-${option.value}`]>
<span class="font-semibold leading-tight">{{ option.label }}</span>
</template>
</Combobox>
<ButtonStyled>
<button class="px-4 py-2" :disabled="isDisabled" @click="setMaxAmount">
{{ formatMessage(commonMessages.maxButton) }}
</button>
</ButtonStyled>
</div>
<div>
<span class="my-1 mt-0 text-secondary">{{ formatMoney(safeMaxAmount) }} available.</span>
<Transition name="fade">
<span v-if="isBelowMinimum" class="text-red">
Amount must be at least {{ formatMoney(minAmount) }}.
</span>
</Transition>
<Transition name="fade">
<span v-if="isAboveMaximum" class="text-red">
Amount cannot exceed {{ formatMoney(safeMaxAmount) }}.
</span>
</Transition>
</div>
</div>
</template>
<script setup lang="ts">
import { ButtonStyled, Combobox, commonMessages, formFieldPlaceholders } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import { computed, nextTick, ref, watch } from 'vue'
const props = withDefaults(
defineProps<{
modelValue: number | undefined
maxAmount: number
minAmount?: number
showCurrencySelector?: boolean
selectedCurrency?: string
currencyOptions?: Array<{ value: string; label: string }>
}>(),
{
minAmount: 0.01,
showCurrencySelector: false,
currencyOptions: () => [],
},
)
const emit = defineEmits<{
'update:modelValue': [value: number | undefined]
'update:selectedCurrency': [value: string]
}>()
const { formatMessage } = useVIntl()
const amountInput = ref<HTMLInputElement | null>(null)
const safeMaxAmount = computed(() => {
return Math.max(0, props.maxAmount)
})
const isDisabled = computed(() => {
return safeMaxAmount.value < 0.01
})
const isBelowMinimum = computed(() => {
return (
props.modelValue !== undefined && props.modelValue > 0 && props.modelValue < props.minAmount
)
})
const isAboveMaximum = computed(() => {
return props.modelValue !== undefined && props.modelValue > safeMaxAmount.value
})
async function setMaxAmount() {
const maxValue = safeMaxAmount.value
emit('update:modelValue', maxValue)
await nextTick()
if (amountInput.value) {
amountInput.value.value = maxValue.toFixed(2)
}
}
function handleInput(event: Event) {
const input = event.target as HTMLInputElement
const value = input.value
if (value && value.includes('.')) {
const parts = value.split('.')
if (parts[1] && parts[1].length > 2) {
const rounded = Math.floor(parseFloat(value) * 100) / 100
emit('update:modelValue', rounded)
input.value = rounded.toString()
return
}
}
const numValue = value === '' ? undefined : parseFloat(value)
emit('update:modelValue', numValue)
}
watch(
() => props.modelValue,
async (newAmount) => {
if (newAmount !== undefined && newAmount !== null) {
if (newAmount > safeMaxAmount.value) {
emit('update:modelValue', safeMaxAmount.value)
await nextTick()
if (amountInput.value) {
amountInput.value.value = safeMaxAmount.value.toFixed(2)
}
} else if (newAmount < 0) {
emit('update:modelValue', 0)
}
}
},
)
</script>
<style scoped>
.fade-enter-active {
transition: opacity 200ms ease-out;
}
.fade-leave-active {
transition: opacity 150ms ease-in;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -1,152 +0,0 @@
<template>
<div class="flex flex-row gap-2 md:gap-3">
<div
class="flex h-10 min-h-10 w-10 min-w-10 justify-center rounded-full border-[1px] border-solid border-button-bg bg-bg-raised !p-0 shadow-md md:h-12 md:min-h-12 md:w-12 md:min-w-12"
>
<ArrowDownIcon v-if="isIncome" class="my-auto size-6 text-secondary md:size-8" />
<ArrowUpIcon v-else class="my-auto size-6 text-secondary md:size-8" />
</div>
<div class="flex w-full flex-row justify-between">
<div class="flex flex-col">
<span class="text-base font-semibold text-contrast md:text-lg">{{
transaction.type === 'payout_available'
? formatPayoutSource(transaction.payout_source)
: formatMethodName(transaction.method_type || transaction.method)
}}</span>
<span class="text-xs text-secondary md:text-sm">
<template v-if="transaction.type === 'withdrawal'">
<span
:class="[
transaction.status === 'cancelling' || transaction.status === 'cancelled'
? 'text-red'
: '',
]"
>{{ formatTransactionStatus(transaction.status) }} <BulletDivider
/></span>
</template>
{{ dayjs(transaction.created).format('MMM DD YYYY') }}
<template v-if="transaction.type === 'withdrawal' && transaction.fee">
<BulletDivider /> Fee {{ formatMoney(transaction.fee) }}
</template>
</span>
</div>
<div class="my-auto flex flex-row items-center gap-2">
<span
class="text-base font-semibold md:text-lg"
:class="transaction.type === 'payout_available' ? 'text-green' : 'text-contrast'"
>{{ formatMoney(transaction.amount) }}</span
>
<template v-if="transaction.type === 'withdrawal' && transaction.status === 'in-transit'">
<Tooltip theme="dismissable-prompt" :triggers="['hover', 'focus']" no-auto-focus>
<span class="my-auto align-middle"
><ButtonStyled circular type="outlined" size="small">
<button class="align-middle" @click="cancelPayout">
<XIcon />
</button> </ButtonStyled
></span>
<template #popper>
<div class="font-semibold text-contrast">Cancel transaction</div>
</template>
</Tooltip>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ArrowDownIcon, ArrowUpIcon, XIcon } from '@modrinth/assets'
import { BulletDivider, ButtonStyled, injectNotificationManager } from '@modrinth/ui'
import { capitalizeString, formatMoney } from '@modrinth/utils'
import dayjs from 'dayjs'
import { Tooltip } from 'floating-vue'
type PayoutStatus = 'in-transit' | 'cancelling' | 'cancelled' | 'success' | 'failed'
type PayoutMethodType = 'paypal' | 'venmo' | 'tremendous' | 'muralpay'
type PayoutSource = 'creator_rewards' | 'affilites'
type WithdrawalTransaction = {
type: 'withdrawal'
id: string
status: PayoutStatus
created: string
amount: number
fee?: number | null
method_type?: PayoutMethodType | null
method?: string
method_id?: string
method_address?: string | null
}
type PayoutAvailableTransaction = {
type: 'payout_available'
created: string
payout_source: PayoutSource
amount: number
}
type Transaction = WithdrawalTransaction | PayoutAvailableTransaction
const props = defineProps<{
transaction: Transaction
}>()
const emit = defineEmits<{
(e: 'cancelled'): void
}>()
const { addNotification } = injectNotificationManager()
const isIncome = computed(() => props.transaction.type === 'payout_available')
function formatTransactionStatus(status: string): string {
if (status === 'in-transit') return 'In Transit'
return capitalizeString(status)
}
function formatMethodName(method: string | undefined): string {
if (!method) return 'Unknown'
switch (method) {
case 'paypal':
return 'PayPal'
case 'venmo':
return 'Venmo'
case 'tremendous':
return 'Tremendous'
case 'muralpay':
return 'Muralpay'
default:
return capitalizeString(method)
}
}
function formatPayoutSource(source: string | undefined): string {
if (!source) return 'Income'
return source
.split('_')
.map((word: string) => capitalizeString(word))
.join(' ')
}
async function cancelPayout(): Promise<void> {
startLoading()
try {
const transaction = props.transaction
if (transaction.type !== 'withdrawal') return
await useBaseFetch(`payout/${transaction.id}`, {
method: 'DELETE',
apiVersion: 3,
})
await useAuth()
emit('cancelled')
} catch (err: any) {
addNotification({
title: 'Failed to cancel transaction',
text: err.data ? err.data.description : err,
type: 'error',
})
}
stopLoading()
}
</script>

View File

@@ -1,119 +0,0 @@
<template>
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-40"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-40"
leave-to-class="opacity-0 max-h-0"
>
<div v-if="amount > 0" class="flex flex-col gap-2.5 rounded-[20px] bg-surface-2 p-4">
<div class="flex items-center justify-between">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownAmount) }}</span>
<span class="font-semibold text-contrast">{{ formatMoney(amount || 0) }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownFee) }}</span>
<span class="h-4 font-semibold text-contrast">
<template v-if="feeLoading">
<LoaderCircleIcon class="size-5 animate-spin !text-secondary" />
</template>
<template v-else>-{{ formatMoney(fee || 0) }}</template>
</span>
</div>
<div class="h-px bg-surface-5" />
<div class="flex items-center justify-between">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownNetAmount) }}</span>
<span class="font-semibold text-contrast">
{{ formatMoney(netAmount) }}
<template v-if="shouldShowExchangeRate">
<span> ({{ formattedLocalCurrency }})</span>
</template>
</span>
</div>
<template v-if="shouldShowExchangeRate">
<div class="flex items-center justify-between text-sm">
<span class="text-primary">{{ formatMessage(messages.feeBreakdownExchangeRate) }}</span>
<span class="text-secondary"
>1 USD = {{ exchangeRate?.toFixed(4) }} {{ localCurrency }}</span
>
</div>
</template>
</div>
</Transition>
</template>
<script setup lang="ts">
import { LoaderCircleIcon } from '@modrinth/assets'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
const props = withDefaults(
defineProps<{
amount: number
fee: number | null
feeLoading: boolean
exchangeRate?: number | null
localCurrency?: string
}>(),
{
exchangeRate: null,
localCurrency: undefined,
},
)
const { formatMessage } = useVIntl()
const netAmount = computed(() => {
const amount = props.amount || 0
const fee = props.fee || 0
return Math.max(0, amount - fee)
})
const shouldShowExchangeRate = computed(() => {
if (!props.localCurrency) return false
if (props.localCurrency === 'USD') return false
return props.exchangeRate !== null && props.exchangeRate !== undefined && props.exchangeRate > 0
})
const netAmountInLocalCurrency = computed(() => {
if (!shouldShowExchangeRate.value) return null
return netAmount.value * (props.exchangeRate || 0)
})
const formattedLocalCurrency = computed(() => {
if (!shouldShowExchangeRate.value || !netAmountInLocalCurrency.value || !props.localCurrency)
return ''
try {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: props.localCurrency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(netAmountInLocalCurrency.value)
} catch {
return `${props.localCurrency} ${netAmountInLocalCurrency.value.toFixed(2)}`
}
})
const messages = defineMessages({
feeBreakdownAmount: {
id: 'dashboard.creator-withdraw-modal.fee-breakdown-amount',
defaultMessage: 'Amount',
},
feeBreakdownFee: {
id: 'dashboard.creator-withdraw-modal.fee-breakdown-fee',
defaultMessage: 'Fee',
},
feeBreakdownNetAmount: {
id: 'dashboard.creator-withdraw-modal.fee-breakdown-net-amount',
defaultMessage: 'Net amount',
},
feeBreakdownExchangeRate: {
id: 'dashboard.creator-withdraw-modal.fee-breakdown-exchange-rate',
defaultMessage: 'FX rate',
},
})
</script>

View File

@@ -1,301 +0,0 @@
<template>
<div class="flex flex-col items-center gap-4">
<div class="flex w-full items-center justify-center gap-2.5">
<span class="text-xl font-semibold text-contrast sm:text-2xl">
{{ formatMessage(messages.title) }}
</span>
</div>
<div class="flex w-full flex-col gap-3">
<div class="span-4 flex w-full flex-col gap-2.5 rounded-2xl bg-surface-2 p-4">
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ formatMessage(messages.method) }}
</span>
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">
{{ result?.methodType || 'N/A' }}
</span>
</div>
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ formatMessage(messages.recipient) }}
</span>
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">
{{ result?.recipientDisplay || 'N/A' }}
</span>
</div>
<div
v-if="destinationLabel && destinationValue"
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ destinationLabel }}
</span>
<span class="break-words font-mono text-sm font-semibold text-contrast sm:text-[1rem]">
{{ destinationValue }}
</span>
</div>
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ formatMessage(messages.date) }}
</span>
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">
{{ formattedDate }}
</span>
</div>
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ formatMessage(messages.amount) }}
</span>
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">
{{ formatMoney(result?.amount || 0) }}
</span>
</div>
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ formatMessage(messages.fee) }}
</span>
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">
{{ formatMoney(result?.fee || 0) }}
</span>
</div>
<div class="border-b-1 h-0 w-full rounded-full border-b border-solid border-divider" />
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ formatMessage(messages.netAmount) }}
</span>
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">
{{ formatMoney(result?.netAmount || 0) }}
<template v-if="shouldShowExchangeRate">
<span> ({{ formattedLocalCurrency }})</span>
</template>
</span>
</div>
<template v-if="shouldShowExchangeRate">
<div
class="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-0"
>
<span class="text-sm font-normal text-primary sm:text-[1rem]">
{{ formatMessage(messages.exchangeRate) }}
</span>
<span class="break-words text-sm font-normal text-secondary sm:text-[1rem]">
1 USD = {{ withdrawData.calculation.exchangeRate?.toFixed(4) }}
{{ localCurrency }}
</span>
</div>
</template>
</div>
<span
v-if="withdrawData.providerData.type === 'tremendous'"
class="w-full break-words text-center text-sm font-normal text-primary sm:text-[1rem]"
>
<IntlFormatted
:message-id="messages.emailConfirmation"
:values="{ email: withdrawData.result?.recipientDisplay }"
>
<template #b="{ children }">
<strong>
<component :is="() => normalizeChildren(children)" />
</strong>
</template>
</IntlFormatted>
</span>
</div>
<Teleport to="body">
<div
v-if="showConfetti"
class="pointer-events-none fixed inset-0 z-[9999] flex items-center justify-center"
>
<ConfettiExplosion />
</div>
</Teleport>
</div>
</template>
<script setup lang="ts">
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import dayjs from 'dayjs'
import { computed, onMounted, ref } from 'vue'
import ConfettiExplosion from 'vue-confetti-explosion'
import { type TremendousProviderData, useWithdrawContext } from '@/providers/creator-withdraw.ts'
import { getRailConfig } from '@/utils/muralpay-rails'
import { normalizeChildren } from '@/utils/vue-children.ts'
const { withdrawData } = useWithdrawContext()
const { formatMessage } = useVIntl()
const result = computed(() => withdrawData.value.result)
const showConfetti = ref(false)
onMounted(() => {
showConfetti.value = true
setTimeout(() => {
showConfetti.value = false
}, 5000)
})
const formattedDate = computed(() => {
if (!result.value?.created) return 'N/A'
return dayjs(result.value.created).format('MMMM D, YYYY')
})
const selectedRail = computed(() => {
const railId = withdrawData.value.selection.method
return railId ? getRailConfig(railId) : null
})
const localCurrency = computed(() => {
// Check if it's Tremendous withdrawal with currency
if (withdrawData.value.providerData.type === 'tremendous') {
return (withdrawData.value.providerData as TremendousProviderData).currency
}
// Fall back to MuralPay rail currency
return selectedRail.value?.currency
})
const shouldShowExchangeRate = computed(() => {
if (!localCurrency.value) return false
if (localCurrency.value === 'USD') return false
const exchangeRate = withdrawData.value.calculation.exchangeRate
return exchangeRate !== null && exchangeRate !== undefined && exchangeRate > 0
})
const netAmountInLocalCurrency = computed(() => {
if (!shouldShowExchangeRate.value) return null
const netAmount = result.value?.netAmount || 0
const exchangeRate = withdrawData.value.calculation.exchangeRate || 0
return netAmount * exchangeRate
})
const formattedLocalCurrency = computed(() => {
if (!shouldShowExchangeRate.value || !netAmountInLocalCurrency.value || !localCurrency.value)
return ''
try {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: localCurrency.value,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(netAmountInLocalCurrency.value)
} catch {
return `${localCurrency.value} ${netAmountInLocalCurrency.value.toFixed(2)}`
}
})
const isMuralPayWithdrawal = computed(() => {
return withdrawData.value.providerData.type === 'muralpay'
})
const destinationLabel = computed(() => {
if (!isMuralPayWithdrawal.value) return null
const rail = selectedRail.value
if (!rail) return null
if (rail.type === 'crypto') {
return formatMessage(messages.wallet)
} else if (rail.type === 'fiat') {
return formatMessage(messages.account)
}
return null
})
const destinationValue = computed(() => {
if (!isMuralPayWithdrawal.value || withdrawData.value.providerData.type !== 'muralpay') {
return null
}
const accountDetails = withdrawData.value.providerData.accountDetails
const rail = selectedRail.value
if (rail?.type === 'crypto' && accountDetails.walletAddress) {
const addr = accountDetails.walletAddress
if (addr.length > 10) {
return `${addr.slice(0, 6)}...${addr.slice(-4)}`
}
return addr
} else if (rail?.type === 'fiat' && accountDetails.bankAccountNumber) {
const accountType = accountDetails.accountType || 'Account'
const last4 = accountDetails.bankAccountNumber.slice(-4)
const formattedType = accountType.charAt(0) + accountType.slice(1).toLowerCase()
return `${formattedType} (${last4})`
}
return null
})
const messages = defineMessages({
title: {
id: 'dashboard.withdraw.completion.title',
defaultMessage: 'Withdraw complete',
},
method: {
id: 'dashboard.withdraw.completion.method',
defaultMessage: 'Method',
},
recipient: {
id: 'dashboard.withdraw.completion.recipient',
defaultMessage: 'Recipient',
},
wallet: {
id: 'dashboard.withdraw.completion.wallet',
defaultMessage: 'Wallet',
},
account: {
id: 'dashboard.withdraw.completion.account',
defaultMessage: 'Account',
},
date: {
id: 'dashboard.withdraw.completion.date',
defaultMessage: 'Date',
},
amount: {
id: 'dashboard.withdraw.completion.amount',
defaultMessage: 'Amount',
},
fee: {
id: 'dashboard.withdraw.completion.fee',
defaultMessage: 'Fee',
},
netAmount: {
id: 'dashboard.withdraw.completion.net-amount',
defaultMessage: 'Net amount',
},
exchangeRate: {
id: 'dashboard.withdraw.completion.exchange-rate',
defaultMessage: 'Exchange rate',
},
emailConfirmation: {
id: 'dashboard.withdraw.completion.email-confirmation',
defaultMessage:
"You'll receive an email at <b>{email}</b> with instructions to redeem your withdrawal.",
},
closeButton: {
id: 'dashboard.withdraw.completion.close-button',
defaultMessage: 'Close',
},
transactionsButton: {
id: 'dashboard.withdraw.completion.transactions-button',
defaultMessage: 'Transactions',
},
})
</script>

View File

@@ -1,352 +0,0 @@
<template>
<div class="flex flex-col gap-4 sm:gap-5">
<div v-if="isPayPal" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast"
>{{ formatMessage(messages.paypalAccount) }} <span class="text-red">*</span></span
>
</label>
<div class="flex flex-col gap-2">
<ButtonStyled v-if="!isPayPalAuthenticated" color="standard">
<a :href="paypalAuthUrl" class="w-min" @click="handlePayPalAuth">
<PayPalColorIcon class="size-5" />
{{ formatMessage(messages.signInWithPaypal) }}
</a>
</ButtonStyled>
<ButtonStyled v-else>
<button class="w-min" @click="handleDisconnectPaypal">
<XIcon /> {{ formatMessage(messages.disconnectButton) }}
</button>
</ButtonStyled>
</div>
</div>
<div v-if="isPayPal && isPayPalAuthenticated" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">{{
formatMessage(messages.account)
}}</span>
</label>
<div class="flex flex-col gap-2 rounded-2xl bg-surface-2 px-4 py-2.5">
<span>{{ paypalEmail }}</span>
</div>
</div>
<div v-if="isVenmo" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast"
>{{ formatMessage(messages.venmoHandle) }} <span class="text-red">*</span></span
>
</label>
<div class="flex flex-row gap-2">
<input
v-model="venmoHandle"
type="text"
:placeholder="formatMessage(messages.venmoHandlePlaceholder)"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
<ButtonStyled color="brand">
<button
v-tooltip="!hasVenmoChanged ? 'Change the venmo username to save.' : undefined"
:disabled="venmoSaving || !hasVenmoChanged"
@click="saveVenmoHandle"
>
<CheckIcon v-if="venmoSaveSuccess" />
<SaveIcon v-else />
{{
venmoSaveSuccess
? formatMessage(messages.savedButton)
: formatMessage(messages.saveButton)
}}
</button>
</ButtonStyled>
</div>
<span v-if="venmoSaveError" class="text-sm font-bold text-red">
{{ venmoSaveError }}
</span>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast"
>{{ formatMessage(formFieldLabels.amount) }} <span class="text-red">*</span></span
>
</label>
<RevenueInputField
v-model="formData.amount"
:max-amount="effectiveMaxAmount"
:min-amount="selectedMethodDetails?.interval?.standard?.min || 0.01"
/>
<WithdrawFeeBreakdown
:amount="formData.amount || 0"
:fee="calculatedFee"
:fee-loading="feeLoading"
/>
<Checkbox v-model="agreedTerms">
<span>
<IntlFormatted :message-id="financialMessages.rewardsProgramTermsAgreement">
<template #terms-link="{ children }">
<nuxt-link to="/legal/cmp" class="text-link">
<component :is="() => normalizeChildren(children)" />
</nuxt-link>
</template>
</IntlFormatted>
</span>
</Checkbox>
</div>
</div>
</template>
<script setup lang="ts">
import { CheckIcon, PayPalColorIcon, SaveIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, Checkbox, financialMessages, formFieldLabels } from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { useDebounceFn } from '@vueuse/core'
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 { useWithdrawContext } from '@/providers/creator-withdraw.ts'
import { normalizeChildren } from '@/utils/vue-children.ts'
const { withdrawData, maxWithdrawAmount, availableMethods, calculateFees, saveStateToStorage } =
useWithdrawContext()
const { formatMessage } = useVIntl()
const auth = await useAuth()
const isPayPal = computed(() => withdrawData.value.selection.provider === 'paypal')
const isVenmo = computed(() => withdrawData.value.selection.provider === 'venmo')
const selectedMethodDetails = computed(() => {
const methodId = withdrawData.value.selection.methodId
if (!methodId) return null
return availableMethods.value.find((m) => m.id === methodId) || null
})
const isPayPalAuthenticated = computed(() => {
return (auth.value.user as any)?.auth_providers?.includes('paypal') || false
})
const paypalEmail = computed(() => {
return (auth.value.user as any)?.payout_data?.paypal_address || ''
})
const paypalAuthUrl = computed(() => {
const route = useRoute()
const authToken = useCookie('auth-token')
const separator = route.fullPath.includes('?') ? '&' : '?'
const returnUrl = `${route.fullPath}${separator}paypal_auth_return=true`
return `${getAuthUrl('paypal', returnUrl)}&auth_token=${authToken.value}`
})
function handlePayPalAuth() {
saveStateToStorage()
}
async function handleDisconnectPaypal() {
try {
await removeAuthProvider('paypal')
} catch (error) {
console.error('Failed to disconnect PayPal:', error)
}
}
const venmoHandle = ref<string>((auth.value.user as any)?.venmo_handle || '')
const initialVenmoHandle = ref<string>((auth.value.user as any)?.venmo_handle || '')
const venmoSaving = ref(false)
const venmoSaveSuccess = ref(false)
const venmoSaveError = ref<string | null>(null)
const hasVenmoChanged = computed(() => {
return venmoHandle.value.trim() !== initialVenmoHandle.value.trim()
})
async function saveVenmoHandle() {
if (!venmoHandle.value.trim()) {
venmoSaveError.value = 'Please enter a Venmo handle'
return
}
venmoSaving.value = true
venmoSaveError.value = null
venmoSaveSuccess.value = false
try {
await useBaseFetch(`user/${(auth.value.user as any)?.id}`, {
method: 'PATCH',
body: {
venmo_handle: venmoHandle.value.trim(),
},
})
// @ts-expect-error auth.js is not typed
await useAuth(auth.value.token)
initialVenmoHandle.value = venmoHandle.value.trim()
venmoSaveSuccess.value = true
setTimeout(() => {
venmoSaveSuccess.value = false
}, 3000)
} catch (error) {
console.error('Failed to update Venmo handle:', error)
venmoSaveError.value = 'Failed to save Venmo handle. Please try again.'
} finally {
venmoSaving.value = false
}
}
const maxAmount = computed(() => maxWithdrawAmount.value)
const roundedMaxAmount = computed(() => Math.floor(maxAmount.value * 100) / 100)
const effectiveMaxAmount = computed(() => {
const apiMax = selectedMethodDetails.value?.interval?.standard?.max
if (apiMax) {
return Math.min(roundedMaxAmount.value, apiMax)
}
return roundedMaxAmount.value
})
const formData = ref<Record<string, any>>({
amount: withdrawData.value.calculation.amount || undefined,
})
const agreedTerms = computed({
get: () => withdrawData.value.agreedTerms,
set: (value) => {
withdrawData.value.agreedTerms = value
},
})
const isComponentValid = computed(() => {
const hasAmount = (formData.value.amount || 0) > 0
const hasAgreed = agreedTerms.value
if (!hasAmount || !hasAgreed) return false
if (isPayPal.value) {
return isPayPalAuthenticated.value
} else if (isVenmo.value) {
return venmoHandle.value.trim().length > 0
}
return false
})
const calculatedFee = ref<number>(0)
const feeLoading = ref(false)
const calculateFeesDebounced = useDebounceFn(async () => {
const amount = formData.value.amount
if (!amount || amount <= 0) {
calculatedFee.value = 0
return
}
const methodId = withdrawData.value.selection.methodId
if (!methodId) {
calculatedFee.value = 0
return
}
feeLoading.value = true
try {
await calculateFees()
calculatedFee.value = withdrawData.value.calculation.fee ?? 0
} catch (error) {
console.error('Failed to calculate fees:', error)
calculatedFee.value = 0
} finally {
feeLoading.value = false
}
}, 500)
watch(
() => formData.value.amount,
() => {
withdrawData.value.calculation.amount = formData.value.amount ?? 0
if (formData.value.amount) {
feeLoading.value = true
calculateFeesDebounced()
} else {
calculatedFee.value = 0
feeLoading.value = false
}
},
{ deep: true },
)
watch(
[isComponentValid, venmoHandle, () => formData.value.amount, agreedTerms, isPayPalAuthenticated],
() => {
withdrawData.value.stageValidation.paypalDetails = isComponentValid.value
},
{ immediate: true },
)
onMounted(async () => {
if (formData.value.amount) {
feeLoading.value = true
calculateFeesDebounced()
}
})
const messages = defineMessages({
paymentMethod: {
id: 'dashboard.creator-withdraw-modal.paypal-details.payment-method',
defaultMessage: 'Payment method',
},
paypalAccount: {
id: 'dashboard.creator-withdraw-modal.paypal-details.paypal-account',
defaultMessage: 'PayPal account',
},
account: {
id: 'dashboard.creator-withdraw-modal.paypal-details.account',
defaultMessage: 'Account',
},
signInWithPaypal: {
id: 'dashboard.creator-withdraw-modal.paypal-details.sign-in-with-paypal',
defaultMessage: 'Sign in with PayPal',
},
paypalAuthDescription: {
id: 'dashboard.creator-withdraw-modal.paypal-details.paypal-auth-description',
defaultMessage: 'Connect your PayPal account to receive payments directly.',
},
venmoHandle: {
id: 'dashboard.creator-withdraw-modal.paypal-details.venmo-handle',
defaultMessage: 'Venmo handle',
},
venmoHandlePlaceholder: {
id: 'dashboard.creator-withdraw-modal.paypal-details.venmo-handle-placeholder',
defaultMessage: '@username',
},
venmoDescription: {
id: 'dashboard.creator-withdraw-modal.paypal-details.venmo-description',
defaultMessage: 'Enter your Venmo handle to receive payments.',
},
disconnectButton: {
id: 'dashboard.creator-withdraw-modal.paypal-details.disconnect-account',
defaultMessage: 'Disconnect account',
},
saveButton: {
id: 'dashboard.creator-withdraw-modal.paypal-details.save-button',
defaultMessage: 'Save',
},
savedButton: {
id: 'dashboard.creator-withdraw-modal.paypal-details.saved-button',
defaultMessage: 'Saved',
},
saveSuccess: {
id: 'dashboard.creator-withdraw-modal.paypal-details.save-success',
defaultMessage: 'Venmo handle saved successfully!',
},
})
</script>

View File

@@ -1,323 +0,0 @@
<template>
<div class="flex flex-col gap-4">
<Admonition v-if="shouldShowTaxLimitWarning" type="warning">
<IntlFormatted
:message-id="messages.taxLimitWarning"
:values="{
amount: formatMoney(maxWithdrawAmount),
}"
>
<template #b="{ children }">
<span class="font-semibold">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
<template #tax-link="{ children }">
<span class="cursor-pointer text-link" @click="onShowTaxForm">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</Admonition>
<div class="flex flex-col">
<div class="flex flex-col gap-2.5">
<div class="flex flex-row gap-1 align-middle">
<span class="align-middle font-semibold text-contrast">{{
formatMessage(messages.region)
}}</span>
<UnknownIcon
v-tooltip="formatMessage(messages.regionTooltip)"
class="mt-auto size-5 text-secondary"
/>
</div>
<Combobox
:model-value="selectedCountryCode"
:options="countries"
:placeholder="formatMessage(messages.countryPlaceholder)"
searchable
:search-placeholder="formatMessage(messages.countrySearchPlaceholder)"
:max-height="240"
class="h-12"
@update:model-value="handleCountryChange"
/>
</div>
<div class="flex flex-col gap-2.5">
<div class="flex flex-row gap-1 align-middle">
<span class="align-middle font-semibold text-contrast">{{
formatMessage(messages.selectMethod)
}}</span>
</div>
<div v-if="loading" class="flex min-h-[120px] items-center justify-center">
<SpinnerIcon class="size-8 animate-spin text-contrast" />
</div>
<template v-else>
<ButtonStyled
v-for="method in paymentOptions"
:key="method.value"
:color="withdrawData.selection.method === method.value ? 'green' : 'standard'"
:highlighted="withdrawData.selection.method === method.value"
type="chip"
>
<button
class="!justify-start !gap-2 !text-left sm:!h-10"
@click="handleMethodSelection(method)"
>
<component :is="method.icon" class="shrink-0" />
<span class="flex-1 truncate text-sm sm:text-[1rem]">
{{ typeof method.label === 'string' ? method.label : formatMessage(method.label) }}
</span>
<span class="ml-auto shrink-0 text-xs font-normal text-secondary sm:text-sm">{{
method.fee
}}</span>
</button>
</ButtonStyled>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { SpinnerIcon, UnknownIcon } from '@modrinth/assets'
import {
Admonition,
ButtonStyled,
Combobox,
injectNotificationManager,
useDebugLogger,
} from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { useGeolocation } from '@vueuse/core'
import { useCountries, useFormattedCountries, useUserCountry } from '@/composables/country.ts'
import { type PayoutMethod, useWithdrawContext } from '@/providers/creator-withdraw.ts'
import { normalizeChildren } from '@/utils/vue-children.ts'
const debug = useDebugLogger('MethodSelectionStage')
const {
withdrawData,
availableMethods,
paymentOptions,
balance,
maxWithdrawAmount,
paymentMethodsCache,
} = useWithdrawContext()
const userCountry = useUserCountry()
const allCountries = useCountries()
const { coords } = useGeolocation()
const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()
const auth = await useAuth()
const messages = defineMessages({
taxLimitWarning: {
id: 'dashboard.creator-withdraw-modal.method-selection.tax-limit-warning',
defaultMessage:
'Your withdraw limit is <b>{amount}</b>, <tax-link>complete a tax form</tax-link> to withdraw more.',
},
region: {
id: 'dashboard.creator-withdraw-modal.method-selection.region',
defaultMessage: 'Region',
},
regionTooltip: {
id: 'dashboard.creator-withdraw-modal.method-selection.region-tooltip',
defaultMessage: 'Some payout methods are not available in certain regions.',
},
countryPlaceholder: {
id: 'dashboard.creator-withdraw-modal.method-selection.country-placeholder',
defaultMessage: 'Select your country',
},
countrySearchPlaceholder: {
id: 'dashboard.creator-withdraw-modal.method-selection.country-search-placeholder',
defaultMessage: 'Search countries...',
},
selectMethod: {
id: 'dashboard.creator-withdraw-modal.method-selection.select-method',
defaultMessage: 'Select withdraw method',
},
errorTitle: {
id: 'dashboard.creator-withdraw-modal.method-selection.error-title',
defaultMessage: 'Failed to load payment methods',
},
errorText: {
id: 'dashboard.creator-withdraw-modal.method-selection.error-text',
defaultMessage: 'Unable to fetch available payment methods. Please try again later.',
},
})
defineProps<{
onShowTaxForm: () => void
}>()
const emit = defineEmits<{
(e: 'close-modal'): void
}>()
const countries = useFormattedCountries()
const selectedCountryCode = computed(() => withdrawData.value.selection.country?.id)
const shouldShowTaxLimitWarning = computed(() => {
const balanceValue = balance.value
if (!balanceValue) return false
const formIncomplete = balanceValue.form_completion_status !== 'complete'
const wouldHitLimit = (balanceValue.withdrawn_ytd ?? 0) + (balanceValue.available ?? 0) >= 600
return formIncomplete && wouldHitLimit
})
const loading = ref(false)
watch(
() => withdrawData.value.selection.country,
async (country) => {
debug('Watch triggered, country:', country)
if (!country) {
availableMethods.value = []
return
}
if (paymentMethodsCache.value[country.id]) {
debug('Using cached methods for', country.id)
availableMethods.value = paymentMethodsCache.value[country.id]
return
}
loading.value = true
debug('Fetching payout methods for country:', country.id)
try {
const methods = (await useBaseFetch('payout/methods', {
apiVersion: 3,
query: { country: country.id },
})) as PayoutMethod[]
debug('Received payout methods:', methods)
paymentMethodsCache.value[country.id] = methods
availableMethods.value = methods
} catch (e) {
console.error('[MethodSelectionStage] Failed to fetch payout methods:', e)
addNotification({
title: formatMessage(messages.errorTitle),
text: formatMessage(messages.errorText),
type: 'error',
})
emit('close-modal')
} finally {
loading.value = false
}
},
{ immediate: true },
)
function handleMethodSelection(option: {
value: string
methodId: string | undefined
type: string
}) {
withdrawData.value.selection.method = option.value
withdrawData.value.selection.methodId = option.methodId ?? null
if (option.type === 'tremendous') {
withdrawData.value.selection.provider = 'tremendous'
} else if (option.type === 'fiat' || option.type === 'crypto') {
withdrawData.value.selection.provider = 'muralpay'
} else if (option.type === 'paypal') {
withdrawData.value.selection.provider = 'paypal'
} else if (option.type === 'venmo') {
withdrawData.value.selection.provider = 'venmo'
} else {
withdrawData.value.selection.provider = 'muralpay'
}
}
watch(paymentOptions, (newOptions) => {
withdrawData.value.selection.method = null
withdrawData.value.selection.methodId = null
withdrawData.value.selection.provider = null
if (newOptions.length === 1) {
const option = newOptions[0]
handleMethodSelection(option)
}
})
watch(
() => withdrawData.value.selection.provider,
(newProvider) => {
if (newProvider === 'tremendous') {
const userEmail = (auth.value.user as any)?.email || ''
withdrawData.value.providerData = {
type: 'tremendous',
deliveryEmail: userEmail,
giftCardDetails: null,
currency: undefined,
}
} else if (newProvider === 'muralpay') {
withdrawData.value.providerData = {
type: 'muralpay',
kycData: {} as any,
accountDetails: {},
}
} else if (newProvider === 'paypal' || newProvider === 'venmo') {
withdrawData.value.providerData = {
type: newProvider,
}
}
},
)
function handleCountryChange(countryCode: string | null) {
debug('handleCountryChange called with:', countryCode)
if (countryCode) {
const normalizedCode = countryCode.toUpperCase()
const country = allCountries.value.find((c) => c.alpha2 === normalizedCode)
debug('Found country:', country)
if (country) {
withdrawData.value.selection.country = {
id: country.alpha2,
name: country.alpha2 === 'TW' ? 'Taiwan' : country.nameShort,
}
debug('Set selectedCountry to:', withdrawData.value.selection.country)
}
} else {
withdrawData.value.selection.country = null
}
}
debug('Setup: userCountry.value =', userCountry.value)
debug('Setup: current selectedCountry =', withdrawData.value.selection.country)
if (!withdrawData.value.selection.country) {
const defaultCountryCode = userCountry.value || 'US'
debug('Setup: calling handleCountryChange with', defaultCountryCode)
handleCountryChange(defaultCountryCode)
debug('Setup: selectedCountryCode computed =', selectedCountryCode.value)
}
async function getCountryFromGeoIP(lat: number, lon: number): Promise<string | null> {
try {
const response = await fetch(
`https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=en`,
)
const data = await response.json()
return data.countryCode || null
} catch {
return null
}
}
onMounted(async () => {
if (withdrawData.value.selection.country?.id === 'US' && !userCountry.value) {
if (coords.value.latitude && coords.value.longitude) {
const geoCountry = await getCountryFromGeoIP(coords.value.latitude, coords.value.longitude)
if (geoCountry) {
handleCountryChange(geoCountry)
}
}
}
})
</script>

View File

@@ -1,516 +0,0 @@
<template>
<div class="flex flex-col gap-3 sm:gap-4">
<Admonition
v-if="selectedRail?.warningMessage"
type="warning"
:header="formatMessage(messages.cryptoWarningHeader)"
>
{{ formatMessage(selectedRail.warningMessage) }}
</Admonition>
<div v-if="selectedRail?.type === 'crypto'" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(messages.coin) }}
</span>
</label>
<div
class="flex min-h-[44px] items-center gap-2 rounded-[14px] bg-surface-2 px-4 py-2.5 sm:min-h-0"
>
<component
:is="getCurrencyIcon(selectedRail.currency)"
class="size-5 shrink-0"
:class="getCurrencyColor(selectedRail.currency)"
/>
<span class="text-sm font-semibold text-contrast sm:text-[1rem]">{{
selectedRail.currency
}}</span>
</div>
</div>
<div v-if="selectedRail?.type === 'fiat'" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(messages.accountOwner) }}
</span>
</label>
<div class="w-full rounded-[14px] bg-surface-2 p-3 sm:p-4">
<div class="flex flex-col gap-1">
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">
{{ accountOwnerName }}
</span>
<span class="break-words text-xs text-primary sm:text-sm">
{{ accountOwnerAddress }}
</span>
</div>
</div>
</div>
<div v-if="selectedRail?.requiresBankName" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.bankName) }}
<span class="text-red">*</span>
</span>
</label>
<Combobox
v-if="shouldShowBankNameDropdown"
v-model="formData.bankName"
:options="bankNameOptions"
:searchable="true"
:placeholder="formatMessage(formFieldPlaceholders.bankNamePlaceholderDropdown)"
class="h-10"
/>
<input
v-else
v-model="formData.bankName"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.bankNamePlaceholder)"
autocomplete="off"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
<div v-for="field in selectedRail?.fields" :key="field.name" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(field.label) }}
<span v-if="field.required" class="text-red">*</span>
</span>
</label>
<input
v-if="['text', 'email', 'tel'].includes(field.type)"
v-model="formData[field.name]"
:type="field.type"
:placeholder="field.placeholder ? formatMessage(field.placeholder) : undefined"
:pattern="field.pattern"
:autocomplete="field.autocomplete || 'off'"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
<Combobox
v-else-if="field.type === 'select'"
v-model="formData[field.name]"
:options="
(field.options || []).map((opt) => ({
value: opt.value,
label: formatMessage(opt.label),
}))
"
:placeholder="field.placeholder ? formatMessage(field.placeholder) : undefined"
class="h-10"
/>
<input
v-else-if="field.type === 'date'"
v-model="formData[field.name]"
type="date"
class="w-full rounded-[14px] bg-surface-4 px-4 py-2.5 text-contrast placeholder:text-secondary"
/>
<span v-if="field.helpText" class="text-sm text-secondary">
{{ formatMessage(field.helpText) }}
</span>
</div>
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-40"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-40"
leave-to-class="opacity-0 max-h-0"
>
<div v-if="dynamicDocumentNumberField" class="overflow-hidden">
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ dynamicDocumentNumberField.label }}
<span v-if="dynamicDocumentNumberField.required" class="text-red">*</span>
</span>
</label>
<input
v-model="formData.documentNumber"
:type="dynamicDocumentNumberField.type"
:placeholder="dynamicDocumentNumberField.placeholder"
autocomplete="off"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
</div>
</Transition>
<div v-if="selectedRail?.blockchain" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(messages.network) }}
</span>
</label>
<div
class="flex min-h-[44px] items-center gap-2 rounded-[14px] bg-surface-2 px-4 py-2.5 sm:min-h-0"
>
<component
:is="getBlockchainIcon(selectedRail.blockchain)"
class="size-5 shrink-0"
:class="getBlockchainColor(selectedRail.blockchain)"
/>
<span class="text-sm font-semibold text-contrast sm:text-[1rem]">{{
selectedRail.blockchain
}}</span>
</div>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.amount) }}
<span class="text-red">*</span>
</span>
</label>
<RevenueInputField
v-model="formData.amount"
:min-amount="effectiveMinAmount"
:max-amount="effectiveMaxAmount"
/>
<WithdrawFeeBreakdown
v-if="allRequiredFieldsFilled"
:amount="formData.amount || 0"
:fee="calculatedFee"
:fee-loading="feeLoading"
:exchange-rate="exchangeRate"
:local-currency="selectedRail?.currency"
/>
<Checkbox v-model="agreedTerms">
<span
><IntlFormatted :message-id="financialMessages.rewardsProgramTermsAgreement">
<template #terms-link="{ children }">
<nuxt-link to="/legal/cmp" class="text-link">
<component :is="() => normalizeChildren(children)" />
</nuxt-link>
</template> </IntlFormatted
></span>
</Checkbox>
</div>
</div>
</template>
<script setup lang="ts">
import {
Admonition,
Checkbox,
Combobox,
financialMessages,
formFieldLabels,
formFieldPlaceholders,
} from '@modrinth/ui'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { useDebounceFn } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import RevenueInputField from '@/components/ui/dashboard/RevenueInputField.vue'
import WithdrawFeeBreakdown from '@/components/ui/dashboard/WithdrawFeeBreakdown.vue'
import { useGeneratedState } from '@/composables/generated'
import { useWithdrawContext } from '@/providers/creator-withdraw.ts'
import {
getBlockchainColor,
getBlockchainIcon,
getCurrencyColor,
getCurrencyIcon,
} from '@/utils/finance-icons.ts'
import { getRailConfig } from '@/utils/muralpay-rails'
import { normalizeChildren } from '@/utils/vue-children.ts'
const { withdrawData, maxWithdrawAmount, availableMethods, calculateFees } = useWithdrawContext()
const { formatMessage } = useVIntl()
const generatedState = useGeneratedState()
const selectedRail = computed(() => {
const railId = withdrawData.value.selection.method
return railId ? getRailConfig(railId) : null
})
const selectedMethodDetails = computed(() => {
const methodId = withdrawData.value.selection.methodId
if (!methodId) return null
return availableMethods.value.find((m) => m.id === methodId) || null
})
const maxAmount = computed(() => maxWithdrawAmount.value)
const roundedMaxAmount = computed(() => Math.floor(maxAmount.value * 100) / 100)
const effectiveMinAmount = computed(
() => selectedMethodDetails.value?.interval?.standard?.min || 0.01,
)
const effectiveMaxAmount = computed(() => {
const apiMax = selectedMethodDetails.value?.interval?.standard?.max
if (apiMax) {
return Math.min(roundedMaxAmount.value, apiMax)
}
return roundedMaxAmount.value
})
const availableBankNames = computed(() => {
const rail = selectedRail.value
if (!rail || !rail.railCode) return []
const bankDetails = generatedState.value.muralBankDetails?.[rail.railCode]
return bankDetails?.bankNames || []
})
const shouldShowBankNameDropdown = computed(() => {
return availableBankNames.value.length > 0
})
const bankNameOptions = computed(() => {
return availableBankNames.value.map((name) => ({
value: name,
label: name,
}))
})
const providerData = withdrawData.value.providerData
const existingAccountDetails = providerData.type === 'muralpay' ? providerData.accountDetails : {}
const existingAmount = withdrawData.value.calculation.amount
const formData = ref<Record<string, any>>({
amount: existingAmount || undefined,
bankName: existingAccountDetails?.bankName ?? '',
...existingAccountDetails,
})
const agreedTerms = computed({
get: () => withdrawData.value.agreedTerms,
set: (value) => {
withdrawData.value.agreedTerms = value
},
})
const calculatedFee = ref<number | null>(null)
const exchangeRate = ref<number | null>(null)
const feeLoading = ref(false)
const hasDocumentTypeField = computed(() => {
const rail = selectedRail.value
if (!rail) return false
return rail.fields.some((field) => field.name === 'documentType')
})
const dynamicDocumentNumberField = computed(() => {
if (!hasDocumentTypeField.value) return null
const documentType = formData.value.documentType
if (!documentType) return null
const labelMap: Record<string, string> = {
NATIONAL_ID: formatMessage(messages.documentNumberNationalId),
PASSPORT: formatMessage(messages.documentNumberPassport),
RESIDENT_ID: formatMessage(messages.documentNumberResidentId),
RUC: formatMessage(messages.documentNumberRuc),
TAX_ID: formatMessage(messages.documentNumberTaxId),
}
const placeholderMap: Record<string, string> = {
NATIONAL_ID: formatMessage(messages.documentNumberNationalIdPlaceholder),
PASSPORT: formatMessage(messages.documentNumberPassportPlaceholder),
RESIDENT_ID: formatMessage(messages.documentNumberResidentIdPlaceholder),
RUC: formatMessage(messages.documentNumberRucPlaceholder),
TAX_ID: formatMessage(messages.documentNumberTaxIdPlaceholder),
}
return {
name: 'documentNumber',
type: 'text' as const,
label: labelMap[documentType] || 'Document number',
placeholder: placeholderMap[documentType] || 'Enter document number',
required: true,
}
})
const accountOwnerName = computed(() => {
const providerDataValue = withdrawData.value.providerData
if (providerDataValue.type !== 'muralpay') return ''
const kycData = providerDataValue.kycData
if (!kycData) return ''
if (kycData.type === 'individual') {
return `${kycData.firstName} ${kycData.lastName}`
} else if (kycData.type === 'business') {
return kycData.name
}
return ''
})
const accountOwnerAddress = computed(() => {
const providerDataValue = withdrawData.value.providerData
if (providerDataValue.type !== 'muralpay') return ''
const kycData = providerDataValue.kycData
if (!kycData || !kycData.physicalAddress) return ''
const addr = kycData.physicalAddress
const parts = [
addr.address1,
addr.address2,
addr.city,
addr.state,
addr.zip,
addr.country,
].filter(Boolean)
return parts.join(', ')
})
const allRequiredFieldsFilled = computed(() => {
const rail = selectedRail.value
if (!rail) return false
const amount = formData.value.amount
if (!amount || amount <= 0) return false
if (rail.requiresBankName && !formData.value.bankName) return false
const requiredFields = rail.fields.filter((f) => f.required)
const allRequiredPresent = requiredFields.every((f) => {
const value = formData.value[f.name]
return value !== undefined && value !== null && value !== ''
})
if (!allRequiredPresent) return false
if (dynamicDocumentNumberField.value?.required && !formData.value.documentNumber) return false
return true
})
const calculateFeesDebounced = useDebounceFn(async () => {
const amount = formData.value.amount
const rail = selectedRail.value
const providerDataValue = withdrawData.value.providerData
const kycData = providerDataValue.type === 'muralpay' ? providerDataValue.kycData : null
if (!amount || amount <= 0 || !rail || !kycData) {
calculatedFee.value = null
exchangeRate.value = null
return
}
feeLoading.value = true
try {
await calculateFees()
calculatedFee.value = withdrawData.value.calculation.fee
exchangeRate.value = withdrawData.value.calculation.exchangeRate
} catch (error) {
console.error('Failed to calculate fees:', error)
calculatedFee.value = 0
exchangeRate.value = null
} finally {
feeLoading.value = false
}
}, 500)
watch(
formData,
() => {
withdrawData.value.calculation.amount = formData.value.amount ?? 0
if (withdrawData.value.providerData.type === 'muralpay') {
withdrawData.value.providerData.accountDetails = { ...formData.value }
}
if (allRequiredFieldsFilled.value) {
feeLoading.value = true
calculateFeesDebounced()
} else {
calculatedFee.value = null
exchangeRate.value = null
feeLoading.value = false
}
},
{ deep: true },
)
if (allRequiredFieldsFilled.value) {
feeLoading.value = true
calculateFeesDebounced()
}
watch(
() => withdrawData.value.selection.method,
(newMethod, oldMethod) => {
if (oldMethod && newMethod !== oldMethod) {
formData.value = {
amount: undefined,
bankName: '',
}
if (withdrawData.value.providerData.type === 'muralpay') {
withdrawData.value.providerData.accountDetails = {}
}
withdrawData.value.calculation.amount = 0
calculatedFee.value = null
exchangeRate.value = null
}
},
)
const messages = defineMessages({
accountOwner: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.account-owner',
defaultMessage: 'Account owner',
},
cryptoWarningHeader: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.crypto-warning-header',
defaultMessage: 'Confirm your wallet address',
},
coin: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.coin',
defaultMessage: 'Coin',
},
network: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.network',
defaultMessage: 'Network',
},
documentNumberNationalId: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-national-id',
defaultMessage: 'National ID number',
},
documentNumberPassport: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-passport',
defaultMessage: 'Passport number',
},
documentNumberResidentId: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-resident-id',
defaultMessage: 'Resident ID number',
},
documentNumberRuc: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-ruc',
defaultMessage: 'RUC number',
},
documentNumberTaxId: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-tax-id',
defaultMessage: 'Tax ID number',
},
documentNumberNationalIdPlaceholder: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-national-id-placeholder',
defaultMessage: 'Enter national ID number',
},
documentNumberPassportPlaceholder: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-passport-placeholder',
defaultMessage: 'Enter passport number',
},
documentNumberResidentIdPlaceholder: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-resident-id-placeholder',
defaultMessage: 'Enter resident ID number',
},
documentNumberRucPlaceholder: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-ruc-placeholder',
defaultMessage: 'Enter RUC number',
},
documentNumberTaxIdPlaceholder: {
id: 'dashboard.creator-withdraw-modal.muralpay-details.document-number-tax-id-placeholder',
defaultMessage: 'Enter tax ID number',
},
})
</script>

View File

@@ -1,362 +0,0 @@
<template>
<div class="flex flex-col gap-3 sm:gap-4">
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(messages.entityQuestion) }}
<span class="text-red">*</span>
</span>
</label>
<Chips
v-model="entityType"
:items="['individual', 'business']"
:format-label="
(item: string) =>
item === 'individual'
? formatMessage(messages.privateIndividual)
: formatMessage(messages.businessEntity)
"
:never-empty="false"
:capitalize="false"
/>
<span class="leading-tight text-primary">
{{ formatMessage(messages.entityDescription) }}
</span>
</div>
<div v-if="entityType" class="flex flex-col gap-4">
<div v-if="entityType === 'business'" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.businessName) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.businessName"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.businessNamePlaceholder)"
autocomplete="organization"
class="w-full rounded-[14px] bg-surface-4 px-4 py-2.5 text-contrast placeholder:text-secondary"
/>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.email) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.email"
type="email"
:placeholder="formatMessage(formFieldPlaceholders.emailPlaceholder)"
autocomplete="email"
class="w-full rounded-[14px] bg-surface-4 px-4 py-2.5 text-contrast placeholder:text-secondary"
/>
</div>
<div v-if="entityType === 'individual'" class="flex flex-col gap-6">
<div class="flex flex-col gap-3 sm:flex-row sm:gap-4">
<div class="flex flex-1 flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.firstName) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.firstName"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.firstNamePlaceholder)"
autocomplete="given-name"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
<div class="flex flex-1 flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.lastName) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.lastName"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.lastNamePlaceholder)"
autocomplete="family-name"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.dateOfBirth) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.dateOfBirth"
type="date"
:max="maxDate"
autocomplete="bday"
class="w-full rounded-[14px] bg-surface-4 px-4 py-2.5 text-contrast placeholder:text-secondary"
/>
</div>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.addressLine) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.physicalAddress.address1"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.addressPlaceholder)"
autocomplete="address-line1"
class="w-full rounded-[14px] bg-surface-4 px-4 py-2.5 text-contrast placeholder:text-secondary"
/>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.addressLine2) }}
</span>
</label>
<input
v-model="formData.physicalAddress.address2"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.address2Placeholder)"
autocomplete="address-line2"
class="w-full rounded-[14px] bg-surface-4 px-4 py-2.5 text-contrast placeholder:text-secondary"
/>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:gap-4">
<div class="flex flex-1 flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.city) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.physicalAddress.city"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.cityPlaceholder)"
autocomplete="address-level2"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
<div class="flex flex-1 flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.stateProvince) }}
<span class="text-red">*</span>
</span>
</label>
<Combobox
v-if="subdivisionOptions.length > 0"
v-model="formData.physicalAddress.state"
:options="subdivisionOptions"
:placeholder="formatMessage(formFieldPlaceholders.statePlaceholder)"
searchable
search-placeholder="Search subdivisions..."
/>
<input
v-else
v-model="formData.physicalAddress.state"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.statePlaceholder)"
autocomplete="address-level1"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:gap-4">
<div class="flex flex-1 flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.postalCode) }}
<span class="text-red">*</span>
</span>
</label>
<input
v-model="formData.physicalAddress.zip"
type="text"
:placeholder="formatMessage(formFieldPlaceholders.postalCodePlaceholder)"
autocomplete="postal-code"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
<div class="flex flex-1 flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(formFieldLabels.country) }}
<span class="text-red">*</span>
</span>
</label>
<Combobox
v-model="formData.physicalAddress.country"
:options="countryOptions"
:placeholder="formatMessage(formFieldPlaceholders.countryPlaceholder)"
searchable
search-placeholder="Search countries..."
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Chips, Combobox, formFieldLabels, formFieldPlaceholders } from '@modrinth/ui'
import { useVIntl } from '@vintl/vintl'
import { useFormattedCountries } from '@/composables/country.ts'
import { useGeneratedState } from '@/composables/generated.ts'
import { useWithdrawContext } from '@/providers/creator-withdraw.ts'
const { withdrawData } = useWithdrawContext()
const { formatMessage } = useVIntl()
const generatedState = useGeneratedState()
const providerData = withdrawData.value.providerData
const existingKycData = providerData.type === 'muralpay' ? providerData.kycData : null
const entityType = ref<'individual' | 'business' | null>(existingKycData?.type ?? null)
interface PayoutRecipientInfoMerged {
email: string
firstName?: string
lastName?: string
dateOfBirth?: string
businessName?: string
physicalAddress: {
address1: string
address2: string | null
country: string
state: string
city: string
zip: string
}
}
const auth = await useAuth()
const formData = ref<PayoutRecipientInfoMerged>({
email: existingKycData?.email ?? `${(auth.value.user as any)?.email}`,
firstName: existingKycData?.type === 'individual' ? existingKycData.firstName : '',
lastName: existingKycData?.type === 'individual' ? existingKycData.lastName : '',
dateOfBirth: existingKycData?.type === 'individual' ? existingKycData.dateOfBirth : '',
businessName: existingKycData?.type === 'business' ? existingKycData.name : '',
physicalAddress: {
address1: existingKycData?.physicalAddress?.address1 ?? '',
address2: existingKycData?.physicalAddress?.address2 ?? null,
country:
existingKycData?.physicalAddress?.country ?? withdrawData.value.selection.country?.id ?? '',
state: existingKycData?.physicalAddress?.state ?? '',
city: existingKycData?.physicalAddress?.city ?? '',
zip: existingKycData?.physicalAddress?.zip ?? '',
},
})
const maxDate = computed(() => {
const today = new Date()
const year = today.getFullYear() - 18
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
})
const countryOptions = useFormattedCountries()
const subdivisionOptions = computed(() => {
const selectedCountry = formData.value.physicalAddress.country
if (!selectedCountry) return []
const subdivisions = generatedState.value.subdivisions?.[selectedCountry] ?? []
return subdivisions.map((sub) => ({
value: sub.code.includes('-') ? sub.code.split('-')[1] : sub.code,
label: sub.localVariant || sub.name,
}))
})
watch(
[entityType, formData],
() => {
if (!entityType.value) {
if (withdrawData.value.providerData.type === 'muralpay') {
withdrawData.value.providerData.kycData = null as any
}
return
}
if (withdrawData.value.providerData.type !== 'muralpay') return
if (entityType.value === 'individual') {
if (formData.value.dateOfBirth) {
withdrawData.value.providerData.kycData = {
type: 'individual',
firstName: formData.value.firstName || '',
lastName: formData.value.lastName || '',
email: formData.value.email,
dateOfBirth: formData.value.dateOfBirth,
physicalAddress: {
address1: formData.value.physicalAddress.address1,
address2: formData.value.physicalAddress.address2 || undefined,
country: formData.value.physicalAddress.country,
state: formData.value.physicalAddress.state,
city: formData.value.physicalAddress.city,
zip: formData.value.physicalAddress.zip,
},
}
}
} else {
withdrawData.value.providerData.kycData = {
type: 'business',
name: formData.value.businessName || '',
email: formData.value.email,
physicalAddress: {
address1: formData.value.physicalAddress.address1,
address2: formData.value.physicalAddress.address2 || undefined,
country: formData.value.physicalAddress.country,
state: formData.value.physicalAddress.state,
city: formData.value.physicalAddress.city,
zip: formData.value.physicalAddress.zip,
},
}
}
},
{ deep: true },
)
const messages = defineMessages({
entityQuestion: {
id: 'dashboard.creator-withdraw-modal.kyc.entity-question',
defaultMessage: 'Are you a withdrawing as an individual or business?',
},
entityDescription: {
id: 'dashboard.creator-withdraw-modal.kyc.entity-description',
defaultMessage:
'A business entity refers to a registered organization such as a corporation, partnership, or LLC.',
},
privateIndividual: {
id: 'dashboard.creator-withdraw-modal.kyc.private-individual',
defaultMessage: 'Private individual',
},
businessEntity: {
id: 'dashboard.creator-withdraw-modal.kyc.business-entity',
defaultMessage: 'Business entity',
},
})
</script>

View File

@@ -1,159 +0,0 @@
<template>
<div class="flex flex-col gap-2.5 sm:gap-3">
<div class="flex flex-col gap-3">
<div class="flex w-full flex-col gap-1 sm:flex-row sm:justify-between sm:gap-0">
<span class="font-semibold text-contrast">{{ formatMessage(messages.withdrawLimit) }}</span>
<div>
<span class="text-orange">{{ formatMoney(usedLimit) }}</span> /
<span class="text-contrast">{{ formatMoney(600) }}</span>
</div>
</div>
<div class="flex h-2.5 w-full overflow-hidden rounded-full bg-surface-2">
<div
v-if="usedLimit > 0"
class="gradient-border bg-orange"
:style="{ width: `${(usedLimit / 600) * 100}%` }"
></div>
</div>
</div>
<template v-if="remainingLimit > 0">
<span>
<IntlFormatted
:message-id="messages.nearingThreshold"
:values="{
amountRemaining: formatMoney(remainingLimit),
}"
>
<template #b="{ children }">
<span class="font-medium">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</span>
<Admonition
type="warning"
show-actions-underneath
:header="formatMessage(messages.taxFormRequiredHeader)"
>
<span class="text-sm font-normal md:text-base">
{{
formatMessage(messages.taxFormRequiredBodyWithLimit, {
limit: formatMoney(remainingLimit),
})
}}
</span>
<template #icon="{ iconClass }">
<FileTextIcon :class="iconClass" />
</template>
<template #actions>
<ButtonStyled color="orange">
<button @click="showTaxFormModal">
{{ formatMessage(messages.completeTaxForm) }}
</button>
</ButtonStyled>
</template>
</Admonition>
</template>
<template v-else>
<span>
<IntlFormatted
:message-id="messages.withdrawLimitUsed"
:values="{ withdrawLimit: formatMoney(600) }"
>
<template #b="{ children }">
<b>
<component :is="() => normalizeChildren(children)" />
</b>
</template>
</IntlFormatted>
</span>
</template>
</div>
</template>
<script setup lang="ts">
import { FileTextIcon } from '@modrinth/assets'
import { Admonition, ButtonStyled } from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { computed } from 'vue'
import { TAX_THRESHOLD_ACTUAL } from '@/providers/creator-withdraw.ts'
import { normalizeChildren } from '@/utils/vue-children.ts'
const props = defineProps<{
balance: any
onShowTaxForm: () => void
}>()
const { formatMessage } = useVIntl()
const usedLimit = computed(() => props.balance?.withdrawn_ytd ?? 0)
const remainingLimit = computed(() => {
const raw = TAX_THRESHOLD_ACTUAL - usedLimit.value
if (raw <= 0) return 0
const cents = Math.floor(raw * 100)
return cents / 100
})
function showTaxFormModal() {
props.onShowTaxForm()
}
const messages = defineMessages({
withdrawLimit: {
id: 'dashboard.creator-withdraw-modal.withdraw-limit',
defaultMessage: 'Withdraw limit',
},
nearingThreshold: {
id: 'dashboard.creator-withdraw-modal.nearing-threshold',
defaultMessage:
"You're nearing the withdraw threshold. You can withdraw <b>{amountRemaining}</b> now, but a tax form is required for more.",
},
taxFormRequiredHeader: {
id: 'dashboard.creator-withdraw-modal.tax-form-required.header',
defaultMessage: 'Tax form required',
},
taxFormRequiredBody: {
id: 'dashboard.creator-withdraw-modal.tax-form-required.body',
defaultMessage:
'To withdraw your full <b>{available}</b> available balance please complete the form below. It is required for tax reporting and only needs to be done once.',
},
taxFormRequiredBodyWithLimit: {
id: 'dashboard.creator-withdraw-modal.tax-form-required.body-with-limit',
defaultMessage:
"You must complete a W-9 or W-8 form for Modrinth's tax records so we remain compliant with tax regulations.",
},
completeTaxForm: {
id: 'dashboard.creator-withdraw-modal.complete-tax-form',
defaultMessage: 'Complete tax form',
},
withdrawLimitUsed: {
id: 'dashboard.creator-withdraw-modal.withdraw-limit-used',
defaultMessage:
"You've used up your <b>{withdrawLimit}</b> withdrawal limit. You must complete a tax form to withdraw more.",
},
})
</script>
<style lang="css" scoped>
.gradient-border {
position: relative;
&::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.3), transparent);
border-radius: inherit;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: xor;
padding: 2px;
pointer-events: none;
}
}
</style>

View File

@@ -1,653 +0,0 @@
<template>
<div class="flex flex-col gap-4 sm:gap-5">
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-40"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-40"
leave-to-class="opacity-0 max-h-0"
>
<div v-if="isUnverifiedEmail" class="overflow-hidden">
<Admonition type="warning" :header="formatMessage(messages.unverifiedEmailHeader)">
{{ formatMessage(messages.unverifiedEmailMessage) }}
</Admonition>
</div>
</Transition>
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-40"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-40"
leave-to-class="opacity-0 max-h-0"
>
<div v-if="shouldShowUsdWarning" class="overflow-hidden">
<Admonition type="warning" :header="formatMessage(messages.usdPaypalWarningHeader)">
<IntlFormatted :message-id="messages.usdPaypalWarningMessage">
<template #direct-paypal-link="{ children }">
<span class="cursor-pointer text-link" @click="switchToDirectPaypal">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</Admonition>
</div>
</Transition>
<div v-if="!showGiftCardSelector && selectedMethodDisplay" class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast">
{{ formatMessage(messages.paymentMethod) }}
</span>
</label>
<div
class="flex min-h-[44px] items-center gap-2 rounded-[14px] bg-surface-2 px-4 py-2.5 sm:min-h-0"
>
<component :is="selectedMethodDisplay.icon" class="size-5 shrink-0" />
<span class="break-words text-sm font-semibold text-contrast sm:text-[1rem]">{{
typeof selectedMethodDisplay.label === 'string'
? selectedMethodDisplay.label
: formatMessage(selectedMethodDisplay.label)
}}</span>
</div>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast"
>{{ formatMessage(formFieldLabels.email) }} <span class="text-red">*</span></span
>
</label>
<input
v-model="deliveryEmail"
type="email"
:placeholder="formatMessage(formFieldPlaceholders.emailPlaceholder)"
autocomplete="email"
class="w-full rounded-[14px] bg-surface-4 px-4 py-3 text-contrast placeholder:text-secondary sm:py-2.5"
/>
</div>
<div v-if="showGiftCardSelector" class="flex flex-col gap-1">
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast"
>{{ categoryLabel }} <span class="text-red">*</span></span
>
</label>
<Combobox
v-model="selectedGiftCardId"
:options="rewardOptions"
:placeholder="`Select ${categoryLabel.toLowerCase()}`"
searchable
:search-placeholder="`Search ${categoryLabelPlural.toLowerCase()}...`"
class="h-10"
>
<template #selected>
<div v-if="selectedRewardOption" class="flex items-center gap-2">
<img
v-if="selectedRewardOption.imageUrl"
:src="selectedRewardOption.imageUrl"
:alt="selectedRewardOption.label"
class="size-5 rounded-full object-cover"
loading="lazy"
/>
<span class="font-semibold leading-tight">{{ selectedRewardOption.label }}</span>
</div>
</template>
<template v-for="option in rewardOptions" :key="option.value" #[`option-${option.value}`]>
<div class="flex items-center gap-2">
<img
v-if="option.imageUrl"
:src="option.imageUrl"
:alt="option.label"
class="size-5 rounded-full object-cover"
loading="lazy"
/>
<span class="font-semibold leading-tight">{{ option.label }}</span>
</div>
</template>
</Combobox>
</div>
<span v-if="selectedMethodDetails" class="text-secondary">
{{ formatMoney(effectiveMinAmount) }} min,
{{ formatMoney(selectedMethodDetails.interval?.standard?.max ?? effectiveMaxAmount) }}
max withdrawal amount.
</span>
</div>
<div class="flex flex-col gap-2.5">
<label>
<span class="text-md font-semibold text-contrast"
>{{ formatMessage(formFieldLabels.amount) }} <span class="text-red">*</span></span
>
</label>
<div v-if="showGiftCardSelector && useFixedDenominations" class="flex flex-col gap-2.5">
<Chips
v-model="selectedDenomination"
:items="denominationOptions"
:format-label="(amt: number) => formatMoney(amt)"
:never-empty="false"
:capitalize="false"
/>
<span v-if="denominationOptions.length === 0" class="text-error text-sm">
No denominations available for your current balance
</span>
</div>
<div v-else class="flex flex-col gap-2">
<RevenueInputField
v-model="formData.amount"
v-model:selected-currency="selectedCurrency"
:max-amount="effectiveMaxAmount"
:min-amount="effectiveMinAmount"
:show-currency-selector="showPayPalCurrencySelector"
:currency-options="currencyOptions"
/>
</div>
<WithdrawFeeBreakdown
v-if="allRequiredFieldsFilled"
:amount="formData.amount || 0"
:fee="calculatedFee"
:fee-loading="feeLoading"
:exchange-rate="exchangeRate"
:local-currency="showPayPalCurrencySelector ? selectedCurrency : undefined"
/>
<Checkbox v-model="agreedTerms">
<span>
<IntlFormatted :message-id="financialMessages.rewardsProgramTermsAgreement">
<template #terms-link="{ children }">
<nuxt-link to="/legal/cmp" class="text-link">
<component :is="() => normalizeChildren(children)" />
</nuxt-link>
</template>
</IntlFormatted>
</span>
</Checkbox>
</div>
</div>
</template>
<script setup lang="ts">
import {
Admonition,
Checkbox,
Chips,
Combobox,
financialMessages,
formFieldLabels,
formFieldPlaceholders,
paymentMethodMessages,
useDebugLogger,
} from '@modrinth/ui'
import { formatMoney } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { IntlFormatted } from '@vintl/vintl/components'
import { useDebounceFn } from '@vueuse/core'
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 { useBaseFetch } from '@/composables/fetch.js'
import { type PayoutMethod, useWithdrawContext } from '@/providers/creator-withdraw.ts'
import { normalizeChildren } from '@/utils/vue-children.ts'
const debug = useDebugLogger('TremendousDetailsStage')
const {
withdrawData,
maxWithdrawAmount,
availableMethods,
paymentOptions,
calculateFees,
setStage,
paymentMethodsCache,
} = useWithdrawContext()
const { formatMessage } = useVIntl()
const auth = await useAuth()
const userEmail = computed(() => {
return (auth.value.user as any)?.email || ''
})
const providerData = withdrawData.value.providerData
const initialDeliveryEmail =
providerData.type === 'tremendous'
? providerData.deliveryEmail || userEmail.value || ''
: userEmail.value || ''
const deliveryEmail = ref<string>(initialDeliveryEmail)
const showGiftCardSelector = computed(() => {
const method = withdrawData.value.selection.method
return method === 'merchant_card' || method === 'charity'
})
const showPayPalCurrencySelector = computed(() => {
const method = withdrawData.value.selection.method
return method === 'paypal'
})
const shouldShowUsdWarning = computed(() => {
const method = withdrawData.value.selection.method
const currency = selectedCurrency.value
return method === 'paypal' && currency === 'USD'
})
const selectedMethodDisplay = computed(() => {
const method = withdrawData.value.selection.method
if (!method) return null
return paymentOptions.value.find((m) => m.value === method) || null
})
const categoryLabel = computed(() => {
const method = withdrawData.value.selection.method
switch (method) {
case 'visa_card':
return formatMessage(paymentMethodMessages.virtualVisa)
case 'merchant_card':
return formatMessage(paymentMethodMessages.giftCard)
case 'charity':
return formatMessage(paymentMethodMessages.charity)
default:
return formatMessage(messages.reward)
}
})
const categoryLabelPlural = computed(() => {
const method = withdrawData.value.selection.method
switch (method) {
case 'visa_card':
return formatMessage(paymentMethodMessages.virtualVisaPlural)
case 'merchant_card':
return formatMessage(paymentMethodMessages.giftCardPlural)
case 'charity':
return formatMessage(paymentMethodMessages.charityPlural)
default:
return formatMessage(messages.rewardPlural)
}
})
const isUnverifiedEmail = computed(() => {
if (!deliveryEmail.value || !userEmail.value) return false
return deliveryEmail.value.toLowerCase() !== userEmail.value.toLowerCase()
})
const maxAmount = computed(() => maxWithdrawAmount.value)
const roundedMaxAmount = computed(() => Math.floor(maxAmount.value * 100) / 100)
const formData = ref<Record<string, any>>({
amount: withdrawData.value.calculation.amount || undefined,
})
const selectedGiftCardId = ref<string | null>(withdrawData.value.selection.methodId || null)
const currencyOptions = [
{ value: 'USD', label: 'USD' },
{ value: 'AUD', label: 'AUD' },
{ value: 'CAD', label: 'CAD' },
{ value: 'CHF', label: 'CHF' },
{ value: 'CZK', label: 'CZK' },
{ value: 'DKK', label: 'DKK' },
{ value: 'EUR', label: 'EUR' },
{ value: 'GBP', label: 'GBP' },
{ value: 'MXN', label: 'MXN' },
{ value: 'NOK', label: 'NOK' },
{ value: 'NZD', label: 'NZD' },
{ value: 'PLN', label: 'PLN' },
{ value: 'SEK', label: 'SEK' },
{ value: 'SGD', label: 'SGD' },
]
function getCurrencyFromCountryCode(countryCode: string | undefined): string {
if (!countryCode) return 'USD'
const code = countryCode.toUpperCase()
const countryToCurrency: Record<string, string> = {
US: 'USD', // United States
GB: 'GBP', // UK
CA: 'CAD', // Canada
AU: 'AUD', // Australia
CH: 'CHF', // Switzerland
CZ: 'CZK', // Czech Republic
DK: 'DKK', // Denmark
MX: 'MXN', // Mexico
NO: 'NOK', // Norway
NZ: 'NZD', // New Zealand
PL: 'PLN', // Poland
SE: 'SEK', // Sweden
SG: 'SGD', // Singapore
// Eurozone countries
AT: 'EUR', // Austria
BE: 'EUR', // Belgium
CY: 'EUR', // Cyprus
EE: 'EUR', // Estonia
FI: 'EUR', // Finland
FR: 'EUR', // France
DE: 'EUR', // Germany
GR: 'EUR', // Greece
IE: 'EUR', // Ireland
IT: 'EUR', // Italy
LV: 'EUR', // Latvia
LT: 'EUR', // Lithuania
LU: 'EUR', // Luxembourg
MT: 'EUR', // Malta
NL: 'EUR', // Netherlands
PT: 'EUR', // Portugal
SK: 'EUR', // Slovakia
SI: 'EUR', // Slovenia
ES: 'EUR', // Spain
}
return countryToCurrency[code] || 'USD'
}
const initialCurrency = getCurrencyFromCountryCode(withdrawData.value.selection.country?.id)
const selectedCurrency = ref<string>(initialCurrency)
const agreedTerms = computed({
get: () => withdrawData.value.agreedTerms,
set: (value) => {
withdrawData.value.agreedTerms = value
},
})
const calculatedFee = ref<number>(0)
const exchangeRate = ref<number | null>(null)
const feeLoading = ref(false)
const rewardOptions = ref<
Array<{
value: string
label: string
imageUrl?: string
methodDetails?: {
id: string
name: string
interval?: {
fixed?: { values: number[] }
standard?: { min: number; max: number }
}
}
}>
>([])
const selectedRewardOption = computed(() => {
if (!selectedGiftCardId.value) return null
return rewardOptions.value.find((opt) => opt.value === selectedGiftCardId.value) || null
})
const selectedMethodDetails = computed(() => {
console.log(rewardOptions.value, selectedGiftCardId.value)
if (!selectedGiftCardId.value) return null
const option = rewardOptions.value.find((opt) => opt.value === selectedGiftCardId.value)
debug('Selected method details:', option?.methodDetails)
return option?.methodDetails || null
})
const useFixedDenominations = computed(() => {
const hasFixed = !!selectedMethodDetails.value?.interval?.fixed?.values
debug('Use fixed denominations:', hasFixed, selectedMethodDetails.value?.interval)
return hasFixed
})
const denominationOptions = computed(() => {
const fixedValues = selectedMethodDetails.value?.interval?.fixed?.values
if (!fixedValues) return []
const filtered = fixedValues
.filter((amount) => amount <= roundedMaxAmount.value)
.sort((a, b) => a - b)
debug(
'Denomination options (filtered by max):',
filtered,
'from',
fixedValues,
'max:',
roundedMaxAmount.value,
)
return filtered
})
const effectiveMinAmount = computed(() => {
return selectedMethodDetails.value?.interval?.standard?.min || 0.01
})
const effectiveMaxAmount = computed(() => {
const methodMax = selectedMethodDetails.value?.interval?.standard?.max
if (methodMax !== undefined && methodMax !== null) {
return Math.min(roundedMaxAmount.value, methodMax)
}
return roundedMaxAmount.value
})
const selectedDenomination = computed({
get: () => formData.value.amount,
set: (value) => {
formData.value.amount = value
},
})
const allRequiredFieldsFilled = computed(() => {
const amount = formData.value.amount
if (!amount || amount <= 0) return false
if (!deliveryEmail.value) return false
if (showGiftCardSelector.value && !selectedGiftCardId.value) return false
return true
})
const calculateFeesDebounced = useDebounceFn(async () => {
const amount = formData.value.amount
if (!amount || amount <= 0) {
calculatedFee.value = 0
exchangeRate.value = null
return
}
const methodId = showGiftCardSelector.value
? selectedGiftCardId.value
: withdrawData.value.selection.methodId
if (!methodId) {
calculatedFee.value = 0
exchangeRate.value = null
return
}
feeLoading.value = true
try {
await calculateFees()
calculatedFee.value = withdrawData.value.calculation.fee ?? 0
exchangeRate.value = withdrawData.value.calculation.exchangeRate
} catch (error) {
console.error('Failed to calculate fees:', error)
calculatedFee.value = 0
exchangeRate.value = null
} finally {
feeLoading.value = false
}
}, 500)
watch(deliveryEmail, (newEmail) => {
if (withdrawData.value.providerData.type === 'tremendous') {
withdrawData.value.providerData.deliveryEmail = newEmail
}
})
watch(
selectedCurrency,
(newCurrency) => {
if (withdrawData.value.providerData.type === 'tremendous') {
;(withdrawData.value.providerData as any).currency = newCurrency
}
},
{ immediate: true },
)
watch(
() => withdrawData.value.selection.country?.id,
(newCountryId) => {
if (showPayPalCurrencySelector.value && newCountryId) {
const detectedCurrency = getCurrencyFromCountryCode(newCountryId)
selectedCurrency.value = detectedCurrency
}
},
)
watch(
[() => formData.value.amount, selectedGiftCardId, deliveryEmail, selectedCurrency],
() => {
withdrawData.value.calculation.amount = formData.value.amount ?? 0
if (showGiftCardSelector.value && selectedGiftCardId.value) {
withdrawData.value.selection.methodId = selectedGiftCardId.value
}
if (allRequiredFieldsFilled.value) {
feeLoading.value = true
calculateFeesDebounced()
} else {
calculatedFee.value = 0
exchangeRate.value = null
feeLoading.value = false
}
},
{ deep: true },
)
onMounted(async () => {
const methods = availableMethods.value
const selectedMethod = withdrawData.value.selection.method
rewardOptions.value = methods
.filter((m) => m.type === 'tremendous')
.filter(
(m) =>
m.category === selectedMethod ||
(selectedMethod === 'merchant_card' && m.category === 'visa_card'),
)
.map((m) => ({
value: m.id,
label: m.name,
imageUrl: m.image_url || m.image_logo_url || undefined,
methodDetails: {
id: m.id,
name: m.name,
interval: m.interval,
},
}))
debug('Loaded reward options:', rewardOptions.value.length, 'methods')
debug('Sample method with interval:', rewardOptions.value[0]?.methodDetails)
if (allRequiredFieldsFilled.value) {
feeLoading.value = true
calculateFeesDebounced()
}
})
watch(
() => withdrawData.value.selection.method,
(newMethod, oldMethod) => {
if (oldMethod && newMethod !== oldMethod) {
formData.value = {
amount: undefined,
}
selectedGiftCardId.value = null
calculatedFee.value = 0
exchangeRate.value = null
// Clear currency when switching away from PayPal International
if (newMethod !== 'paypal' && withdrawData.value.providerData.type === 'tremendous') {
;(withdrawData.value.providerData as any).currency = undefined
}
}
},
)
async function switchToDirectPaypal() {
withdrawData.value.selection.country = {
id: 'US',
name: 'United States',
}
let usMethods = paymentMethodsCache.value['US']
if (!usMethods) {
try {
usMethods = (await useBaseFetch('payout/methods', {
apiVersion: 3,
query: { country: 'US' },
})) as PayoutMethod[]
paymentMethodsCache.value['US'] = usMethods
} catch (error) {
console.error('Failed to fetch US payment methods:', error)
return
}
}
availableMethods.value = usMethods
const directPaypal = usMethods.find((m) => m.type === 'paypal')
if (directPaypal) {
withdrawData.value.selection.provider = 'paypal'
withdrawData.value.selection.method = directPaypal.id
withdrawData.value.selection.methodId = directPaypal.id
withdrawData.value.providerData = {
type: 'paypal',
}
await setStage('paypal-details', true)
} else {
console.error('An error occured - no paypal in US region??')
}
}
const messages = defineMessages({
unverifiedEmailHeader: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.unverified-email-header',
defaultMessage: 'Unverified email',
},
unverifiedEmailMessage: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.unverified-email-message',
defaultMessage:
'The delivery email you have entered is not associated with your Modrinth account. Modrinth cannot recover rewards sent to an incorrect email address.',
},
paymentMethod: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.payment-method',
defaultMessage: 'Payment method',
},
reward: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.reward',
defaultMessage: 'Reward',
},
rewardPlaceholder: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.reward-placeholder',
defaultMessage: 'Select reward',
},
rewardPlural: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.reward-plural',
defaultMessage: 'Rewards',
},
usdPaypalWarningHeader: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.usd-paypal-warning-header',
defaultMessage: 'Lower fees available',
},
usdPaypalWarningMessage: {
id: 'dashboard.creator-withdraw-modal.tremendous-details.usd-paypal-warning-message',
defaultMessage:
'You selected USD for PayPal International. <direct-paypal-link>Switch to direct PayPal</direct-paypal-link> for better fees (≈2% instead of ≈6%).',
},
})
</script>

View File

@@ -139,7 +139,7 @@ const messages = defineMessages({
resubmitForReviewDesc: {
id: 'project-moderation-nags.resubmit-for-review-desc',
defaultMessage:
"Your project has been {status, select, rejected {rejected} withheld {withheld} other {{status}}} by Modrinth's staff. In most cases, you can resubmit for review after addressing the staff's message.",
"Your project has been {status} by Modrinth's staff. In most cases, you can resubmit for review after addressing the staff's message.",
},
visitModerationPage: {
id: 'project-moderation-nags.visit-moderation-page',

View File

@@ -85,7 +85,6 @@
<ModpackPermissionsFlow
v-model="modpackJudgements"
:project-id="project.id"
:project-updated="project.updated"
@complete="handleModpackPermissionsComplete"
/>
</div>
@@ -447,11 +446,6 @@ function resetProgress() {
localStorage.removeItem(`modpack-permissions-${props.project.id}`)
localStorage.removeItem(`modpack-permissions-index-${props.project.id}`)
sessionStorage.removeItem(`modpack-permissions-data-${props.project.id}`)
sessionStorage.removeItem(`modpack-permissions-permanent-no-${props.project.id}`)
sessionStorage.removeItem(`modpack-permissions-updated-${props.project.id}`)
modpackPermissionsComplete.value = false
modpackJudgements.value = {}
@@ -1337,11 +1331,6 @@ function clearProjectLocalStorage() {
localStorage.removeItem(`moderation-actions-${props.project.slug}`)
localStorage.removeItem(`moderation-inputs-${props.project.slug}`)
localStorage.removeItem(`moderation-stage-${props.project.slug}`)
sessionStorage.removeItem(`modpack-permissions-data-${props.project.id}`)
sessionStorage.removeItem(`modpack-permissions-permanent-no-${props.project.id}`)
sessionStorage.removeItem(`modpack-permissions-updated-${props.project.id}`)
actionStates.value = {}
}

View File

@@ -161,7 +161,6 @@ import { computed, onMounted, ref, watch } from 'vue'
const props = defineProps<{
projectId: string
projectUpdated: string
modelValue?: ModerationJudgements
}>()
@@ -203,10 +202,6 @@ const permanentNoFiles = useSessionStorage<ModerationModpackItem[]>(
},
},
)
const cachedProjectUpdated = useSessionStorage<string | null>(
`modpack-permissions-updated-${props.projectId}`,
null,
)
const currentIndex = ref(0)
const fileApprovalTypes: ModerationModpackPermissionApprovalType[] = [
@@ -368,13 +363,11 @@ async function fetchModPackData(): Promise<void> {
}
modPackData.value = sortedData
cachedProjectUpdated.value = props.projectUpdated
persistAll()
} catch (error) {
console.error('Failed to fetch modpack data:', error)
modPackData.value = []
permanentNoFiles.value = []
cachedProjectUpdated.value = props.projectUpdated
persistAll()
}
}
@@ -464,10 +457,7 @@ function getJudgements(): ModerationJudgements {
onMounted(() => {
loadPersistedData()
const isStale = cachedProjectUpdated.value !== props.projectUpdated
if (!modPackData.value || isStale) {
if (!modPackData.value) {
fetchModPackData()
}
})
@@ -487,7 +477,6 @@ watch(
() => props.projectId,
() => {
clearPersistedData()
cachedProjectUpdated.value = null
loadPersistedData()
if (!modPackData.value) {
fetchModPackData()

View File

@@ -105,9 +105,10 @@
<script setup>
import { ReportIcon, UnknownIcon, VersionIcon } from '@modrinth/assets'
import { Avatar, Badge, CopyCode, useRelativeTime } from '@modrinth/ui'
import { formatProjectType, renderHighlightedString } from '@modrinth/utils'
import { formatProjectType } from '@modrinth/utils'
import ThreadSummary from '~/components/ui/thread/ThreadSummary.vue'
import { renderHighlightedString } from '~/helpers/highlight.js'
import { getProjectTypeForUrl } from '~/helpers/projects.js'
const formatRelativeTime = useRelativeTime()

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