Merge branch 'feat/i18n' into feat/merge-big-stuff

This commit is contained in:
Maya 2025-07-26 20:58:02 +03:00
commit 0e8c191768
No known key found for this signature in database
37 changed files with 852 additions and 259 deletions

5
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"inlang.vs-code-extension"
]
}

View File

@ -24,6 +24,7 @@
"vite-plugin-wasm": "^3.4.1",
},
"devDependencies": {
"@inlang/paraglide-js": "2.2.0",
"@poppanator/sveltekit-svg": "^5.0.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.16.0",
@ -140,6 +141,12 @@
"@imagemagick/magick-wasm": ["@imagemagick/magick-wasm@0.0.34", "", {}, "sha512-vSTe4cfR8U8r2Ityo6s3iRFVK903W4fhur3t/ixc6vLSAJXmqXC9cKXz2NkkDBUVU7ssJTYMiTKAb1S9RY+ZdQ=="],
"@inlang/paraglide-js": ["@inlang/paraglide-js@2.2.0", "", { "dependencies": { "@inlang/recommend-sherlock": "0.2.1", "@inlang/sdk": "2.4.9", "commander": "11.1.0", "consola": "3.4.0", "json5": "2.2.3", "unplugin": "^2.1.2", "urlpattern-polyfill": "^10.0.0" }, "bin": { "paraglide-js": "bin/run.js" } }, "sha512-pkpXu1LanvpcAbvpVPf7PgF11Uq7DliSEBngrcUN36l4ZOOpzn3QBTvVr/tJxvks0O67WseQgiMHet8KH7Oz5A=="],
"@inlang/recommend-sherlock": ["@inlang/recommend-sherlock@0.2.1", "", { "dependencies": { "comment-json": "^4.2.3" } }, "sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg=="],
"@inlang/sdk": ["@inlang/sdk@2.4.9", "", { "dependencies": { "@lix-js/sdk": "0.4.7", "@sinclair/typebox": "^0.31.17", "kysely": "^0.27.4", "sqlite-wasm-kysely": "0.3.0", "uuid": "^10.0.0" } }, "sha512-cvz/C1rF5WBxzHbEoiBoI6Sz6q6M+TdxfWkEGBYTD77opY8i8WN01prUWXEM87GPF4SZcyIySez9U0Ccm12oFQ=="],
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
@ -152,6 +159,10 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@lix-js/sdk": ["@lix-js/sdk@0.4.7", "", { "dependencies": { "@lix-js/server-protocol-schema": "0.1.1", "dedent": "1.5.1", "human-id": "^4.1.1", "js-sha256": "^0.11.0", "kysely": "^0.27.4", "sqlite-wasm-kysely": "0.3.0", "uuid": "^10.0.0" } }, "sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ=="],
"@lix-js/server-protocol-schema": ["@lix-js/server-protocol-schema@0.1.1", "", {}, "sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
@ -236,6 +247,10 @@
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
"@sinclair/typebox": ["@sinclair/typebox@0.31.28", "", {}, "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ=="],
"@sqlite.org/sqlite-wasm": ["@sqlite.org/sqlite-wasm@3.48.0-build4", "", { "bin": { "sqlite-wasm": "bin/index.js" } }, "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ=="],
"@stripe/stripe-js": ["@stripe/stripe-js@7.4.0", "", {}, "sha512-lQHQPfXPTBeh0XFjq6PqSBAyR7umwcJbvJhXV77uGCUDD6ymXJU/f2164ydLMLCCceNuPlbV9b+1smx98efwWQ=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="],
@ -322,6 +337,8 @@
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"array-timsort": ["array-timsort@1.0.3", "", {}, "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ=="],
"autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@ -358,14 +375,20 @@
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"comment-json": ["comment-json@4.2.5", "", { "dependencies": { "array-timsort": "^1.0.3", "core-util-is": "^1.0.3", "esprima": "^4.0.1", "has-own-prop": "^2.0.0", "repeat-string": "^1.6.1" } }, "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="],
@ -380,6 +403,8 @@
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"dedent": ["dedent@1.5.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
@ -432,6 +457,8 @@
"espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
"esrap": ["esrap@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g=="],
@ -490,8 +517,12 @@
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-own-prop": ["has-own-prop@2.0.0", "", {}, "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"human-id": ["human-id@4.1.1", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ieee754-buffer": ["ieee754-buffer@2.0.0", "", {}, "sha512-AXUAT0nMEi7h1Is8HXGXof3eejl/GabZFKSj8Ym6kVRUSwrAb52EkAXywiCQYSHGQMRn7lvfY7vhPMjVc+Kybg=="],
@ -528,6 +559,8 @@
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
"js-sha256": ["js-sha256@0.11.1", "", {}, "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
@ -536,12 +569,16 @@
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"known-css-properties": ["known-css-properties@0.35.0", "", {}, "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A=="],
"kysely": ["kysely@0.27.6", "", {}, "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
@ -668,6 +705,8 @@
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="],
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
@ -698,6 +737,8 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"sqlite-wasm-kysely": ["sqlite-wasm-kysely@0.3.0", "", { "dependencies": { "@sqlite.org/sqlite-wasm": "^3.48.0-build2" }, "peerDependencies": { "kysely": "*" } }, "sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg=="],
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@ -750,10 +791,14 @@
"uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
"unplugin": ["unplugin@2.3.5", "", { "dependencies": { "acorn": "^8.14.1", "picomatch": "^4.0.2", "webpack-virtual-modules": "^0.6.2" } }, "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw=="],
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
"utf8-buffer": ["utf8-buffer@1.0.0", "", {}, "sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
@ -770,6 +815,8 @@
"vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="],
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
@ -822,6 +869,8 @@
"svelte-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
"svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
"tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"tailwindcss/postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="],

189
messages/en.json Normal file
View File

@ -0,0 +1,189 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"navbar": {
"upload": "Upload",
"convert": "Convert",
"settings": "Settings",
"about": "About",
"toggle_theme": "Toggle theme"
},
"footer": {
"copyright": "© {year} VERT.",
"source_code": "Source code",
"discord_server": "Discord server"
},
"upload": {
"title": "The file converter you'll love.",
"subtitle": "All image, audio, and document processing is done on your device. Videos are converted on our lightning-fast servers. No file size limit, no ads, and completely open source.",
"uploader": {
"text": "Drop or click to {action}",
"convert": "convert",
"jpegify": "jpegify"
},
"cards": {
"title": "VERT supports...",
"images": "Images",
"audio": "Audio",
"documents": "Documents",
"video": "Video",
"video_server_processing": "Video uploads to a server for processing by default, learn how to set it up locally [wiki_link]here[/wiki_link].",
"local_supported": "Local fully supported",
"status": {
"text": "<b>Status:</b> {status}",
"ready": "ready",
"not_ready": "not ready"
},
"supported_formats": "Supported formats:"
},
"tooltip": {
"partial_support": "This format can only be converted as {direction}.",
"direction_input": "input (from)",
"direction_output": "output (to)"
}
},
"convert": {
"panel": {
"convert_all": "Convert all",
"download_all": "Download all as .zip",
"remove_all": "Remove all files",
"set_all_to": "Set all to",
"na": "N/A"
},
"tooltips": {
"unknown_file": "Unknown file type",
"audio_file": "Audio file",
"video_file": "Video file",
"document_file": "Document file",
"image_file": "Image file",
"convert_file": "Convert this file",
"download_file": "Download this file"
},
"errors": {
"cant_convert": "We can't convert this file.",
"vertd_server": "what are you doing..? you're supposed to run the vertd server!",
"unsupported_format": "Only image, video, audio, and document files are supported",
"vertd_not_found": "Could not find the vertd instance to start video conversion. Are you sure the instance URL is set correctly?"
}
},
"settings": {
"title": "Settings",
"errors": {
"save_failed": "Failed to save settings!"
},
"appearance": {
"title": "Appearance",
"brightness_theme": "Brightness theme",
"brightness_description": "Want a sunny flash-bang, or a quiet lonely night?",
"light": "Light",
"dark": "Dark",
"effect_settings": "Effect settings",
"effect_description": "Would you like fancy effects, or a more static experience?",
"enable": "Enable",
"disable": "Disable"
},
"conversion": {
"title": "Conversion",
"filename_format": "File name format",
"filename_description": "This will determine the name of the file on download, <b>not including the file extension.</b> You can put these following templates in the format, which will be replaced with the relevant information: <b>%name%</b> for the original file name, <b>%extension%</b> for the original file extension, and <b>%date%</b> for a date string of when the file was converted.",
"placeholder": "VERT_%name%"
},
"vertd": {
"title": "Video conversion",
"status": "status:",
"loading": "loading...",
"available": "available, commit id {commitId}",
"unavailable": "unavailable (is the url right?)",
"description": "The <code>vertd</code> project is a server wrapper for FFmpeg. This allows you to convert videos through the convenience of VERT's web interface, while still being able to harness the power of your GPU to do it as quickly as possible.",
"hosting_info": "We host a public instance for your convenience, but it is quite easy to host your own on your PC or server if you know what you are doing. You can download the server binaries [vertd_link]here[/vertd_link] - the process of setting this up will become easier in the future, so stay tuned!",
"instance_url": "Instance URL",
"url_placeholder": "Example: http://localhost:24153",
"conversion_speed": "Conversion speed",
"speed_description": "This describes the tradeoff between speed and quality. Faster speeds will result in lower quality, but will get the job done quicker.",
"speeds": {
"very_slow": "Very Slow",
"slower": "Slower",
"slow": "Slow",
"medium": "Medium",
"fast": "Fast",
"ultra_fast": "Ultra Fast"
}
},
"privacy": {
"title": "Privacy",
"plausible_title": "Plausible analytics",
"plausible_description": "We use [plausible_link]Plausible[/plausible_link], a privacy-focused analytics tool, to gather completely anonymous statistics. All data is anonymized and aggregated, and no identifiable information is ever sent or stored. You can view the analytics [analytics_link]here[/analytics_link] and choose to opt out below.",
"opt_in": "Opt-in",
"opt_out": "Opt-out"
},
"language": {
"title": "Language",
"description": "Select your preferred language for the VERT interface."
}
},
"about": {
"title": "About",
"why": {
"title": "Why VERT?",
"description": "<b>File converters have always disappointed us.</b> They're ugly, riddled with ads, and most importantly; slow. We decided to solve this problem once and for all by making an alternative that solves all those problems, and more.<br/><br/>All non-video files are converted completely on-device; this means that there's no delay between sending and receiving the files from a server, and we never get to snoop on the files you convert.<br/><br/>Video files get uploaded to our lightning-fast RTX 4000 Ada server. Your videos stay on there for an hour if you do not convert them. If you do convert the file, the video will stay on the server for an hour, or until it is downloaded. The file will then be deleted from our server."
},
"sponsors": {
"title": "Sponsors",
"description": "Want to support us? Contact a developer in the [discord_link]Discord[/discord_link] server, or send an email to",
"email_copied": "Email copied to clipboard!"
},
"resources": {
"title": "Resources",
"discord": "Discord",
"source": "Source",
"email": "Email"
},
"donate": {
"title": "Donate to VERT",
"description": "With your support, we can keep maintaining and improving VERT.",
"one_time": "One-time",
"monthly": "Monthly",
"custom": "Custom",
"pay_now": "Pay now",
"donate_amount": "Donate ${amount} USD",
"thank_you": "Thank you for your donation!",
"payment_failed": "Payment failed: {message}{period} You have not been charged.",
"donation_error": "An error occurred while processing your donation. Please try again later.",
"payment_error": "Error fetching payment details. Please try again later."
},
"credits": {
"title": "Credits",
"contact_team": "If you would like to contact the development team, please use the email found on the \"Resources\" card.",
"notable_contributors": "Notable contributors",
"notable_description": "We'd like to thank these people for their major contributions to VERT.",
"github_contributors": "GitHub contributors",
"github_description": "Big [jpegify_link]thanks[/jpegify_link] to all these people for helping out! [github_link]Want to help too?[/github_link]",
"no_contributors": "Seems like no one has contributed yet... [contribute_link]be the first to contribute![/contribute_link]",
"libraries": "Libraries",
"libraries_description": "A big thanks to FFmpeg (audio, video), ImageMagick (images) and Pandoc (documents) for maintaining such excellent libraries for so many years. VERT relies on them to provide you with your conversions.",
"roles": {
"lead_developer": "Lead developer; conversion backend, UI implementation",
"developer": "Developer; UI implementation",
"designer": "Designer; UX, branding, marketing",
"docker_ci": "Maintaining Docker & CI support",
"former_cofounder": "Former co-founder & designer"
}
},
"errors": {
"github_contributors": "Error fetching GitHub contributors"
}
},
"workers": {
"errors": {
"general": "Error converting file {file}: {message}",
"magick": "Error in Magick worker, image conversion may not work as expected.`",
"ffmpeg": "Error loading ffmpeg, some features may not work."
}
},
"jpegify": {
"title": "SECRET JPEGIFY!!!",
"subtitle": "(shh... don't tell anyone!)",
"button": "JPEGIFY {compression}%!!!",
"download": "Download",
"delete": "Delete"
}
}

189
messages/uwu.json Normal file
View File

@ -0,0 +1,189 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"upload": {
"title": "The fiwe conwewtew you'ww wuv.",
"subtitle": "Aww image, audio, and document pwocessing is donye on youw dewice. Videos awe conwewted on ouw wightnying-fast sewwews. Nyo fiwe size wimit, nyo ads, and compwetewy open souwce.",
"uploader": {
"text": "Dwop or cwick to {action}",
"convert": "conwewt",
"jpegify": "jpegify"
},
"cards": {
"title": "VEWT suppowts...",
"images": "Images",
"audio": "Audio",
"documents": "Documents",
"video": "Video",
"video_server_processing": "Video upwoads to a sewwew fow pwocessing by defauwt, weawn how to set it up wocawwy [wiki_link]hewe[/wiki_link].",
"local_supported": "Wocaw fuwwy suppowted",
"status": {
"text": "<b>Status:</b> {status}",
"ready": "weady",
"not_ready": "nyot weady"
},
"supported_formats": "Suppowted fowmats:"
},
"tooltip": {
"partial_support": "This fowmat can onwy be conwewted as {direction}.",
"direction_input": "input (fwom)",
"direction_output": "output (to)"
}
},
"convert": {
"panel": {
"convert_all": "Conwewt aww",
"download_all": "Downwoad aww as .zip",
"remove_all": "Wemuv aww fiwes",
"set_all_to": "Set aww to",
"na": "N/A"
},
"tooltips": {
"unknown_file": "Unknyown fiwe type",
"audio_file": "Audio fiwe",
"video_file": "Video fiwe",
"document_file": "Document fiwe",
"image_file": "Image fiwe",
"convert_file": "Conwewt this fiwe",
"download_file": "Downwoad this fiwe"
},
"errors": {
"cant_convert": "We can't conwewt this fiwe.",
"vertd_server": "what awe you doing..? you'we supposed to wun the wewtd sewwew!",
"unsupported_format": "Onwy image, wideo, audio, and document fiwes awe suppowted",
"vertd_not_found": "Couwd nyot find the wewtd instance to stawt wideo conwewsion. Awe you suwe the instance UWW is set cowwectwy?"
}
},
"settings": {
"title": "Settings",
"errors": {
"save_failed": "Faiwed to sawe settings!"
},
"appearance": {
"title": "Appeawance",
"brightness_theme": "Bwightnyess theme",
"brightness_description": "Want a sunny fwash-bang, ow a quiet wonyewy nyight?",
"light": "Wight",
"dark": "Dawk",
"effect_settings": "Effect settings",
"effect_description": "Wouwd you wike fancy effects, ow a mowe static expewience?",
"enable": "Enyabwe",
"disable": "Disabwe"
},
"conversion": {
"title": "Conwewsion",
"filename_format": "Fiwe nyame fowmat",
"filename_description": "This wiww detewminye the nyame of the fiwe on downwoad, <b>nyot incwuding the fiwe extension.</b> You can put these fowwowing tempwates in the fowmat, which wiww be wepwaced with the wewewant infowmation: <b>%nyame%</b> fow the owiginyaw fiwe nyame, <b>%extension%</b> fow the owiginyaw fiwe extension, and <b>%date%</b> fow a date stwing of when the fiwe was conwewted.",
"placeholder": "VEWT_%nyame%"
},
"vertd": {
"title": "Video conwewsion",
"status": "status:",
"loading": "woading...",
"available": "awaiwabwe, commit id {commitId}",
"unavailable": "unyawaiwabwe (is the uww wight?)",
"description": "The <code>wewtd</code> pwojyect is a sewwew wwappew fow FFmpeg. This awwows you to conwewt wideos thwough the conwenyience of VEWT's web intewface, whiwe stiww being abwe to hawnyess the powew of youw GPU to do it as quickwy as possibwe.",
"hosting_info": "We host a pubwic instance fow youw conwenyience, but it is quite easy to host youw own on youw PC ow sewwew if you knyow what you awe doing. You can downwoad the sewwew binyawies [vertd_link]hewe[/vertd_link] - the pwocess of setting this up wiww become easiew in the futuwe, so stay tunyed!",
"instance_url": "Instance UWW",
"url_placeholder": "Exampwe: http://wocawhost:24153",
"conversion_speed": "Conwewsion speed",
"speed_description": "This descwibes the twadeoff between speed and quawity. Fastew speeds wiww wesuwt in wowew quawity, but wiww get the jyob donye quickew.",
"speeds": {
"very_slow": "Vewy Swow",
"slower": "Swowew",
"slow": "Swow",
"medium": "Medium",
"fast": "Fast",
"ultra_fast": "Uwtwa Fast"
}
},
"privacy": {
"title": "Pwiwacy",
"plausible_title": "Pwausibwe anyawytics",
"plausible_description": "We use [plausible_link]Pwausibwe[/plausible_link], a pwiwacy-focused anyawytics toow, to gathew compwetewy anyonymous statistics. Aww data is anyonymized and aggwegated, and nyo identifiabwe infowmation is ewew sent ow stowed. You can wiew the anyawytics [analytics_link]hewe[/analytics_link] and choose to opt out bewow.",
"opt_in": "Opt-in",
"opt_out": "Opt-out"
},
"language": {
"title": "Wanguage",
"description": "Sewect youw pwefewwed wanguage fow the VEWT intewface."
}
},
"about": {
"title": "About",
"why": {
"title": "Why VEWT?",
"description": "<b>Fiwe conwewtews hawe awways disappointed us.</b> They'we ugwy, widdwed with ads, and most impowtantwy; swow. We decided to sowwe this pwobwem once and fow aww by making an awtewnyatiwe that sowwes aww those pwobwems, and mowe.<br/><br/>Aww nyon-wideo fiwes awe conwewted compwetewy on-dewice; this means that thewe's nyo deway between sending and weceiwing the fiwes fwom a sewwew, and we nyewew get to snyoop on the fiwes you conwewt.<br/><br/>Video fiwes get upwoaded to ouw wightnying-fast WTX 4000 Ada sewwew. Youw wideos stay on thewe fow an houw if you do nyot conwewt them. If you do conwewt the fiwe, the wideo wiww stay on the sewwew fow an houw, ow untiw it is downwoaded. The fiwe wiww then be deweted fwom ouw sewwew."
},
"sponsors": {
"title": "Sponsows",
"description": "Want to suppowt us? Contact a dewewopew in the [discord_link]Discowd[/discord_link] sewwew, ow send an emaiw to",
"email_copied": "Emaiw copied to cwipboawd!"
},
"resources": {
"title": "Wesouwces",
"discord": "Discowd",
"source": "Souwce",
"email": "Emaiw"
},
"donate": {
"title": "Donyate to VEWT",
"description": "With youw suppowt, we can keep maintainying and impwowing VEWT.",
"one_time": "Onye-time",
"monthly": "Monthwy",
"custom": "Custom",
"pay_now": "Pay nyow",
"donate_amount": "Donyate ${amount} USD",
"thank_you": "Thank you fow youw donyation!",
"payment_failed": "Payment faiwed: {message}{period} You hawe nyot been chawged.",
"donation_error": "An ewwow occuwwed whiwe pwocessing youw donyation. Pwease twy again watew.",
"payment_error": "Ewwow fetching payment detaiws. Pwease twy again watew."
},
"credits": {
"title": "Cwedits",
"contact_team": "If you wouwd wike to contact the dewewopment team, pwease use the emaiw found on the \"Wesouwces\" cawd.",
"notable_contributors": "Nyotabwe contwibutows",
"notable_description": "We'd wike to thank these peopwe fow theiw majyow contwibutions to VEWT.",
"github_contributors": "GitHub contwibutows",
"github_description": "Big [jpegify_link]thanks[/jpegify_link] to aww these peopwe fow hewping out! [github_link]Want to hewp too?[/github_link]",
"no_contributors": "Seems wike nyo onye has contwibuted yet... [contribute_link]be the fiwst to contwibute![/contribute_link]",
"libraries": "Wibwawies",
"libraries_description": "A beeg thankies to FFmpeg (audio, wideo), ImageMagick (images) and Pandoc (documents) fow maintainying such excewwent wibwawies fow so many yeaws. VEWT wewies on them to pwowide you with youw conwewsions.",
"roles": {
"lead_developer": "Wead dewewopew; conwewsion backend, UI impwementation",
"developer": "Dewewopew; UI impwementation",
"designer": "Designyew; UX, bwanding, mawketing",
"docker_ci": "Maintainying Dockew & CI suppowt",
"former_cofounder": "Fowmew co-foundew & designyew"
}
},
"errors": {
"github_contributors": "Ewwow fetching GitHub contwibutows"
}
},
"workers": {
"errors": {
"general": "Ewwow conwewting fiwe {file}: {message}",
"magick": "Ewwow in Magick wowkew, image conwewsion may nyot wowk as expected.`",
"ffmpeg": "Ewwow woading ffmpeg, some featuwes may nyot wowk."
}
},
"jpegify": {
"title": "SECWET JPEGIFY!!!",
"subtitle": "(shh... don't teww anyonye!)",
"button": "JPEGIFY {compression}%!!!",
"download": "Downwoad",
"delete": "Dewete"
},
"navbar": {
"upload": "Upwoad",
"convert": "Conwewt",
"settings": "Settings",
"about": "About",
"toggle_theme": "Toggwe theme"
},
"footer": {
"copyright": "© {year} VEWT.",
"source_code": "Souwce code",
"discord_server": "Discowd sewwew"
}
}

View File

@ -33,7 +33,8 @@
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0",
"vite": "^5.4.11",
"vite-plugin-top-level-await": "^1.5.0"
"vite-plugin-top-level-await": "^1.5.0",
"@inlang/paraglide-js": "2.2.0"
},
"dependencies": {
"@bjorn3/browser_wasi_shim": "^0.4.1",

1
project.inlang/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
cache

View File

@ -0,0 +1 @@
ff77Td2rnvEqQyzBYT

View File

@ -0,0 +1,15 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"baseLocale": "en",
"locales": [
"en",
"uwu"
],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json"
}
}

View File

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="%lang%">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

15
src/hooks.server.ts Normal file
View File

@ -0,0 +1,15 @@
import type { Handle } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
// creating a handle to use the paraglide middleware
const paraglideHandle: Handle = ({ event, resolve }) =>
paraglideMiddleware(event.request, ({ request: localizedRequest, locale }) => {
event.request = localizedRequest;
return resolve(event, {
transformPageChunk: ({ html }) => {
return html.replace('%lang%', locale);
}
});
});
export const handle: Handle = paraglideHandle;

6
src/hooks.ts Normal file
View File

@ -0,0 +1,6 @@
import type { Reroute } from '@sveltejs/kit';
import { deLocalizeUrl } from '$lib/paraglide/runtime';
export const reroute: Reroute = (request) => {
return deLocalizeUrl(request.url).pathname;
};

View File

@ -7,6 +7,7 @@
import ProgressBar from "../visual/ProgressBar.svelte";
import FormatDropdown from "./FormatDropdown.svelte";
import { categories } from "$lib/converters";
import { m } from "$lib/paraglide/messages";
const length = $derived(files.files.length);
const progress = $derived(files.files.filter((f) => f.result).length);
@ -27,7 +28,7 @@
disabled={!files.ready}
>
<RefreshCw size="24" />
<p>Convert all</p>
<p>{m["convert.panel.convert_all"]()}</p>
</button>
<button
class="btn {$effects
@ -37,7 +38,7 @@
onclick={() => files.downloadAll()}
>
<FolderArchiveIcon size="24" />
<p>Download all as .zip</p>
<p>{m["convert.panel.download_all"]()}</p>
</button>
{#if $isMobile}
<button
@ -48,10 +49,10 @@
onclick={() => (files.files = [])}
>
<Trash2Icon size="24" />
<p>Remove all files</p>
<p>{m["convert.panel.remove_all"]()}</p>
</button>
{:else}
<Tooltip text="Remove all files" position="right">
<Tooltip text={m["convert.panel.remove_all"]()} position="right">
<button
class="btn p-4 {$effects
? ''
@ -66,7 +67,7 @@
</div>
<div class="w-full bg-separator h-0.5 flex md:hidden"></div>
<div class="flex items-center gap-2">
<p class="whitespace-nowrap text-xl">Set all to</p>
<p class="whitespace-nowrap text-xl">{m["convert.panel.set_all_to"]()}</p>
{#if files.requiredConverters.length === 1}
<FormatDropdown
onselect={(r) =>
@ -79,7 +80,7 @@
{categories}
/>
{:else}
<Dropdown options={["N/A"]} disabled />
<Dropdown options={[m["convert.panel.na"]()]} disabled />
{/if}
</div>
</div>

View File

@ -1,5 +1,6 @@
<script lang="ts">
import { duration, fade, transition } from "$lib/animation";
import { m } from "$lib/paraglide/messages";
import { isMobile, files } from "$lib/store/index.svelte";
import type { Categories } from "$lib/types";
import { ChevronDown, SearchIcon } from "lucide-svelte";
@ -240,7 +241,7 @@
: 'border-b-separator text-muted'}"
onclick={() => selectCategory(category)}
>
{category}
{(m as any)[`upload.cards.${category}`]?.() || category}
</button>
{/each}
</div>

View File

@ -7,6 +7,7 @@
import { converters } from "$lib/converters";
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { m } from "$lib/paraglide/messages";
type Props = {
class?: string;
@ -98,7 +99,11 @@
<UploadIcon class="w-full h-full text-on-accent" />
</div>
<h2 class="text-center text-2xl font-semibold mt-4">
Drop or click to {jpegify ? "JPEGIFY" : "convert"}
{m["upload.uploader.text"]({
action: jpegify
? m["upload.uploader.jpegify"]()
: m["upload.uploader.convert"]()
})}
</h2>
</Panel>
</button>

View File

@ -1,11 +1,11 @@
<script lang="ts">
import { GITHUB_URL_VERT, DISCORD_URL } from "$lib/consts";
import { m } from "$lib/paraglide/messages";
const items = Object.entries({
//"Privacy policy": "#",
"Source code": GITHUB_URL_VERT,
"Discord server": DISCORD_URL,
});
const items = $derived([
[m["footer.source_code"](), GITHUB_URL_VERT],
[m["footer.discord_server"](), DISCORD_URL],
]);
const year = new Date().getFullYear();
</script>
@ -16,7 +16,7 @@
<div
class="w-full h-full flex items-center justify-center text-muted gap-3 relative"
>
<p>© {year} VERT.</p>
<p>{m["footer.copyright"]({ year })}</p>
{#each items as [name, url] (name)}
<!-- bullet point -->
<p></p>

View File

@ -22,6 +22,7 @@
import Logo from "../../visual/svg/Logo.svelte";
import { beforeNavigate } from "$app/navigation";
import Tooltip from "$lib/components/visual/Tooltip.svelte";
import { m } from "$lib/paraglide/messages";
const items = $derived<
{
@ -33,13 +34,13 @@
}[]
>([
{
name: "Upload",
name: m["navbar.upload"](),
url: "/",
activeMatch: (pathname) => pathname === "/",
icon: UploadIcon,
},
{
name: "Convert",
name: m["navbar.convert"](),
url: "/convert/",
activeMatch: (pathname) =>
pathname === "/convert/" || pathname === "/convert",
@ -47,13 +48,13 @@
badge: files.files.length,
},
{
name: "Settings",
name: m["navbar.settings"](),
url: "/settings/",
activeMatch: (pathname) => pathname.startsWith("/settings"),
icon: SettingsIcon,
},
{
name: "About",
name: m["navbar.about"](),
url: "/about/",
activeMatch: (pathname) => pathname.startsWith("/about"),
icon: InfoIcon,
@ -180,7 +181,7 @@
{@render link(item, i)}
{/each}
<div class="w-0.5 bg-separator h-full hidden md:flex"></div>
<Tooltip text="Toggle theme" position="right">
<Tooltip text={m["navbar.toggle_theme"]()} position="right">
<button
onclick={() => {
const isDark =

View File

@ -4,6 +4,7 @@ import { FFmpeg } from "@ffmpeg/ffmpeg";
import { browser } from "$app/environment";
import { error, log } from "$lib/logger";
import { addToast } from "$lib/store/ToastProvider";
import { m } from "$lib/paraglide/messages";
export class FFmpegConverter extends Converter {
private ffmpeg: FFmpeg = null!;
@ -49,7 +50,7 @@ export class FFmpegConverter extends Converter {
error(["converters", this.name], `error loading ffmpeg: ${err}`);
addToast(
"error",
`Error loading ffmpeg, some features may not work.`,
m["workers.errors.ffmpeg"](),
);
}
}

View File

@ -1,5 +1,6 @@
import { browser } from "$app/environment";
import { error, log } from "$lib/logger";
import { m } from "$lib/paraglide/messages";
import { addToast } from "$lib/store/ToastProvider";
import type { OmitBetterStrict, WorkerMessage } from "$lib/types";
import { VertFile } from "$lib/types";
@ -71,7 +72,7 @@ export class MagickConverter extends Converter {
);
addToast(
"error",
`Error in Magick worker, image conversion may not work as expected.`,
m["workers.errors.magick"](),
);
throw new Error(message.error);
}

View File

@ -2,6 +2,8 @@
import Panel from "$lib/components/visual/Panel.svelte";
import { HeartHandshakeIcon } from "lucide-svelte";
import { GITHUB_URL_VERT } from "$lib/consts";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
let { mainContribs, notableContribs, ghContribs } = $props();
</script>
@ -51,12 +53,11 @@
<div class="rounded-full bg-blue-300 p-2 inline-block mr-3 w-10 h-10">
<HeartHandshakeIcon color="black" />
</div>
Credits
{m["about.credits.title"]()}
</h2>
<p class="-mt-4 -mb-3 font-black text-lg">
If you would like to contact the development team, please use the email
found on the "Resources" card.
{m["about.credits.contact_team"]()}
</p>
<!-- Main contributors -->
@ -72,11 +73,12 @@
<!-- Notable contributors -->
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<h2 class="text-base font-bold">Notable contributors</h2>
<h2 class="text-base font-bold">
{m["about.credits.notable_contributors"]()}
</h2>
<div class="flex flex-col gap-2">
<p class="text-base text-muted font-normal">
We'd like to thank these people for their major
contributions to VERT.
{m["about.credits.notable_description"]()}
</p>
<div class="flex flex-col gap-2">
{#each notableContribs as contrib}
@ -90,34 +92,26 @@
<!-- GitHub contributors -->
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<h2 class="text-base font-bold">GitHub contributors</h2>
<h2 class="text-base font-bold">
{m["about.credits.github_contributors"]()}
</h2>
{#if ghContribs && ghContribs.length > 0}
<p class="text-base text-muted font-normal">
Big <a
class="text-black dynadark:text-white"
href="/jpegify">thanks</a
>
to all these people for helping out!
<a
class="text-blue-500 font-normal hover:underline"
href={GITHUB_URL_VERT}
target="_blank"
rel="noopener noreferrer"
>
Want to help too?
</a>
{@html link(
["jpegify_link", "github_link"],
m["about.credits.github_description"](),
["/jpegify", GITHUB_URL_VERT],
[false, true],
["text-black dynadark:text-white", "text-blue-500 font-normal hover:underline"]
)}
</p>
{:else}
<p class="text-base text-muted font-normal italic">
Seems like no one has contributed yet...
<a
class="text-blue-500 font-normal hover:underline"
href={GITHUB_URL_VERT}
target="_blank"
rel="noopener noreferrer"
>
be the first to contribute!
</a>
{@html link(
"contribute_link",
m["about.credits.no_contributors"](),
GITHUB_URL_VERT,
)}
</p>
{/if}
</div>
@ -131,12 +125,9 @@
</div>
{/if}
<h2 class="mt-2 -mb-2">Libraries</h2>
<h2 class="mt-2 -mb-2">{m["about.credits.libraries"]()}</h2>
<p class="font-normal">
A big thanks to FFmpeg (audio, video), Imagemagick (images) and
Pandoc (documents) for maintaining such excellent libraries for
so many years. VERT relies on them to provide you with your
conversions.
{m["about.credits.libraries_description"]()}
</p>
</div>
</div></Panel

View File

@ -35,9 +35,10 @@
HeartIcon,
WalletIcon,
} from "lucide-svelte";
import { onMount, tick } from "svelte";
import { onMount } from "svelte";
import { Elements, PaymentElement } from "svelte-stripe";
import { quintOut } from "svelte/easing";
import { m } from "$lib/paraglide/messages";
let amount = $state(1);
let customAmount = $state("");
@ -68,7 +69,7 @@
paymentState = "prepay";
addToast(
"error",
"Error fetching payment details. Please try again later.",
m["about.donate.payment_error"](),
);
return;
}
@ -98,9 +99,13 @@
const submitResult = await elements.submit();
if (submitResult.error) {
const period = submitResult.error.message?.endsWith(".") ? "" : ".";
addToast(
"error",
`Payment failed: ${submitResult.error.message}${submitResult.error.message?.endsWith(".") ? "" : "."} You have not been charged.`,
m["about.donate.payment_failed"]({
message: submitResult.error.message || "",
period
}),
);
enablePay = true;
return;
@ -116,12 +121,16 @@
});
if (res.error) {
const period = res.error.message?.endsWith(".") ? "" : ".";
addToast(
"error",
`Payment failed: ${res.error.message}${res.error.message?.endsWith(".") ? "" : "."} You have not been charged.`,
m["about.donate.payment_failed"]({
message: res.error.message || "",
period
}),
);
} else {
addToast("success", "Thank you for your donation!");
addToast("success", m["about.donate.thank_you"]());
}
paymentState = "prepay";
@ -140,12 +149,12 @@
if (status) {
switch (status) {
case "succeeded":
addToast("success", "Thank you for your donation!");
addToast("success", m["about.donate.thank_you"]());
break;
default:
addToast(
"error",
"An error occurred while processing your donation. Please try again later.",
m["about.donate.donation_error"](),
);
}
@ -162,10 +171,10 @@
>
<HeartIcon color="black" />
</div>
Donate to VERT
{m["about.donate.title"]()}
</h2>
<p class="text-base font-normal">
With your support, we can keep maintaining and improving VERT.
{m["about.donate.description"]()}
</p>
</div>
@ -192,7 +201,7 @@
)}
>
<HandCoinsIcon size="24" class="inline-block mr-2" />
One-time
{m["about.donate.one_time"]()}
</button>
<button
@ -207,7 +216,7 @@
)}
>
<CalendarHeartIcon size="24" class="inline-block mr-2" />
Monthly
{m["about.donate.monthly"]()}
</button>
</div>
<div class="grid grid-cols-4 gap-3 w-full">
@ -229,7 +238,7 @@
<div class="flex items-center justify-center">
<FancyInput
bind:value={customAmount}
placeholder="Custom"
placeholder={m["about.donate.custom"]()}
prefix="$"
type="number"
/>
@ -288,7 +297,7 @@
class="btn w-full h-12 bg-accent-red text-black rounded-full mt-4"
onclick={donate}
>
Donate ${amount.toFixed(2)} USD
{m["about.donate.donate_amount"]({ amount: amount.toFixed(2) })}
</button>
</div>
</div>
@ -304,7 +313,7 @@
class="row-start-1 col-start-1 flex justify-center items-center"
>
<WalletIcon size="24" class="inline-block mr-2" />
Pay now
{m["about.donate.pay_now"]()}
</div>
{/if}
</div>

View File

@ -8,6 +8,7 @@
MailIcon,
MessageCircleMoreIcon,
} from "lucide-svelte";
import { m } from "$lib/paraglide/messages";
</script>
<Panel class="flex flex-col gap-4 p-6">
@ -17,7 +18,7 @@
>
<LinkIcon color="black" />
</div>
Resources
{m["about.resources.title"]()}
</h2>
<div class="flex gap-3">
<a
@ -29,7 +30,7 @@
: '!scale-100'} flex-1 gap-2 p-4 rounded-full bg-button text-black dynadark:text-white flex items-center justify-center"
>
<MessageCircleMoreIcon size="24" class="inline-block mr-2" />
Discord
{m["about.resources.discord"]()}
</a>
<a
href={GITHUB_URL_VERT}
@ -40,7 +41,7 @@
: '!scale-100'} flex-1 gap-2 p-4 rounded-full bg-button text-black dynadark:text-white flex items-center justify-center"
>
<GithubIcon size="24" class="inline-block mr-2" />
Source
{m["about.resources.source"]()}
</a>
<a
href="mailto:{CONTACT_EMAIL}"
@ -51,7 +52,7 @@
: '!scale-100'} flex-1 gap-2 p-4 rounded-full bg-button text-black dynadark:text-white flex items-center justify-center"
>
<MailIcon size="24" class="inline-block mr-2" />
Email
{m["about.resources.email"]()}
</a>
</div>
</Panel>

View File

@ -5,6 +5,8 @@
import { DISCORD_URL } from "$lib/consts";
import { error } from "$lib/logger";
import { addToast } from "$lib/store/ToastProvider";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
let copied = false;
let timeoutId: number | undefined;
@ -13,7 +15,7 @@
try {
navigator.clipboard.writeText("hello@vert.sh");
copied = true;
addToast("success", "Email copied to clipboard!");
addToast("success", m["about.sponsors.email_copied"]());
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => (copied = false), 2000);
@ -30,7 +32,7 @@
>
<PiggyBankIcon color="black" />
</div>
Sponsors
{m["about.sponsors.title"]()}
</h2>
<div class="mt-2 [&>*]:font-normal h-full flex justify-between flex-col">
<div class="flex gap-3 justify-center text-lg">
@ -43,11 +45,11 @@
</a>
</div>
<p class="text-muted">
Want to support us? Contact a developer in the <a
href={DISCORD_URL}
target="_blank">Discord</a
>
server, or send an email to
{@html link(
"discord_link",
m["about.sponsors.description"](),
DISCORD_URL,
)}
<span class="inline-block mx-[2px] relative top-[2px]">
<button
id="email"

View File

@ -1,6 +1,7 @@
<script lang="ts">
import Panel from "$lib/components/visual/Panel.svelte";
import { MessageCircleQuestionIcon } from "lucide-svelte";
import { m } from "$lib/paraglide/messages";
</script>
<Panel class="flex flex-col gap-3 p-6">
@ -10,22 +11,9 @@
>
<MessageCircleQuestionIcon color="black" />
</div>
Why VERT?
{m["about.why.title"]()}
</h2>
<p class="text-lg font-normal">
<b>File converters have always disappointed us.</b> They're ugly,
riddled with ads, and most importantly; slow. We decided to solve this
problem once and for all by making an alternative that solves all those
problems, and more.<br />
<br />
All non-video files are converted completely on-device; this means that there's
no delay between sending and receiving the files from a server, and we never
get to snoop on the files you convert.
<br />
<br />
Video files get uploaded to our lightning-fast RTX 4000 Ada server. Your
videos stay on there for an hour if you do not convert them. If you do convert
the file, the video will stay on the server for an hour, or until it is downloaded.
The file will then be deleted from our server.
{@html m["about.why.description"]()}
</p>
</Panel>

View File

@ -5,6 +5,8 @@
effects,
setEffects,
setTheme,
updateLocale,
availableLocales,
} from "$lib/store/index.svelte";
import {
MoonIcon,
@ -14,6 +16,23 @@
SunIcon,
} from "lucide-svelte";
import { onMount, onDestroy } from "svelte";
import { m } from "$lib/paraglide/messages";
import { getLocale } from "$lib/paraglide/runtime";
import Dropdown from "$lib/components/functional/Dropdown.svelte";
let currentLocale = $state("en");
const getLanguageDisplayName = (locale: string) => {
try {
return availableLocales[locale as keyof typeof availableLocales];
} catch {
return locale.toUpperCase();
}
};
const languageOptions = Object.keys(availableLocales).map((locale) =>
getLanguageDisplayName(locale),
);
let lightElement: HTMLButtonElement;
let darkElement: HTMLButtonElement;
@ -49,6 +68,8 @@
onMount(() => {
effectsUnsubscribe = effects.subscribe(updateEffectsClasses);
themeUnsubscribe = theme.subscribe(updateThemeClasses);
currentLocale = localStorage.getItem("locale") || getLocale();
});
onDestroy(() => {
@ -60,6 +81,17 @@
updateEffectsClasses($effects);
updateThemeClasses($theme);
});
function handleLanguageChange(selectedLanguage: string) {
const selectedLocale = Object.keys(availableLocales).find(
(locale) => getLanguageDisplayName(locale) === selectedLanguage,
);
if (selectedLocale && selectedLocale !== currentLocale) {
currentLocale = selectedLocale;
updateLocale(selectedLocale);
}
}
</script>
<Panel class="flex flex-col gap-8 p-6">
@ -70,14 +102,16 @@
class="inline-block -mt-1 mr-2 bg-accent-purple p-2 rounded-full"
color="black"
/>
Appearance
{m["settings.appearance.title"]()}
</h2>
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">Brightness theme</p>
<p class="text-base font-bold">
{m["settings.appearance.brightness_theme"]()}
</p>
<p class="text-sm text-muted font-normal italic">
Want a sunny flash-bang, or a quiet lonely night?
{m["settings.appearance.brightness_description"]()}
</p>
</div>
<div class="flex flex-col gap-3 w-full">
@ -85,29 +119,34 @@
<button
bind:this={lightElement}
onclick={() => setTheme("light")}
class="btn {$effects ? "" : "!scale-100"} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
class="btn {$effects
? ''
: '!scale-100'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<SunIcon size="24" class="inline-block mr-2" />
Light
{m["settings.appearance.light"]()}
</button>
<button
bind:this={darkElement}
onclick={() => setTheme("dark")}
class="btn {$effects ? "" : "!scale-100"} flex-1 p-4 rounded-lg text-black flex items-center justify-center"
class="btn {$effects
? ''
: '!scale-100'} flex-1 p-4 rounded-lg text-black flex items-center justify-center"
>
<MoonIcon size="24" class="inline-block mr-2" />
Dark
{m["settings.appearance.dark"]()}
</button>
</div>
</div>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">Effect settings</p>
<p class="text-base font-bold">
{m["settings.appearance.effect_settings"]()}
</p>
<p class="text-sm text-muted font-normal italic">
Would you like fancy effects, or a more static
experience?
{m["settings.appearance.effect_description"]()}
</p>
</div>
<div class="flex flex-col gap-3 w-full">
@ -115,23 +154,45 @@
<button
bind:this={enableEffectsElement}
onclick={() => setEffects(true)}
class="btn {$effects ? "" : "!scale-100"} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
class="btn {$effects
? ''
: '!scale-100'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<PlayIcon size="24" class="inline-block mr-2" />
Enable
{m["settings.appearance.enable"]()}
</button>
<button
bind:this={disableEffectsElement}
onclick={() => setEffects(false)}
class="btn {$effects ? "" : "!scale-100"} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
class="btn {$effects
? ''
: '!scale-100'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<PauseIcon size="24" class="inline-block mr-2" />
Disable
{m["settings.appearance.disable"]()}
</button>
</div>
</div>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">
{m["settings.language.title"]()}
</p>
<p class="text-sm text-muted font-normal italic">
{m["settings.language.description"]()}
</p>
</div>
<div class="flex flex-col gap-3 w-full">
<Dropdown
options={languageOptions}
settingsStyle
selected={getLanguageDisplayName(currentLocale)}
onselect={handleLanguageChange}
/>
</div>
</div>
</div>
</div>
</Panel>

View File

@ -3,6 +3,7 @@
import Panel from "$lib/components/visual/Panel.svelte";
import { RefreshCwIcon } from "lucide-svelte";
import type { ISettings } from "./index.svelte";
import { m } from "$lib/paraglide/messages";
const { settings }: { settings: ISettings } = $props();
</script>
@ -15,25 +16,14 @@
class="inline-block -mt-1 mr-2 bg-accent p-2 rounded-full"
color="black"
/>
Conversion
{m["settings.conversion.title"]()}
</h2>
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">File name format</p>
<p class="text-base font-bold">{m["settings.conversion.filename_format"]()}</p>
<p class="text-sm text-muted font-normal">
This will determine the name of the file on download, <span
class="font-bold italic"
>not including the file extension.</span
>
You can put these following templates in the format, which
will be replaced with the relevant information:
<span class="font-bold">%name%</span>
for the original file name,
<span class="font-bold">%extension%</span>
for the original file extension, and
<span class="font-bold">%date%</span>
for a date string of when the file was converted.
{@html m["settings.conversion.filename_description"]()}
</p>
</div>
<FancyTextInput

View File

@ -3,6 +3,8 @@
import { ChartColumnIcon, PauseIcon, PlayIcon } from "lucide-svelte";
import type { ISettings } from "./index.svelte";
import { effects } from "$lib/store/index.svelte";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
const { settings }: { settings: ISettings } = $props();
</script>
@ -15,26 +17,23 @@
class="inline-block -mt-1 mr-2 bg-accent-blue p-2 rounded-full"
color="black"
/>
Privacy
{m["settings.privacy.title"]()}
</h2>
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">Plausible analytics</p>
<p class="text-base font-bold">
{m["settings.privacy.plausible_title"]()}
</p>
<p class="text-sm text-muted font-normal">
We use <a
href="https://plausible.io/privacy-focused-web-analytics"
target="_blank"
rel="noopener noreferrer">Plausible</a
>, a privacy-focused analytics tool, to gather
completely anonymous statistics. All data is anonymized
and aggregated, and no identifiable information is ever
sent or stored. You can view the analytics
<a
href="https://ats.vert.sh/vert.sh"
target="_blank"
rel="noopener noreferrer">here</a
> and choose to opt out below.
{@html link(
["plausible_link", "analytics_link"],
m["settings.privacy.plausible_description"](),
[
"https://plausible.io/privacy-focused-web-analytics",
"https://ats.vert.sh/vert.sh",
],
)}
</p>
</div>
<div class="flex flex-col gap-3 w-full">
@ -48,7 +47,7 @@
: ''} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<PlayIcon size="24" class="inline-block mr-2" />
Opt-in
{m["settings.privacy.opt_in"]()}
</button>
<button
@ -60,7 +59,7 @@
: 'selected'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<PauseIcon size="24" class="inline-block mr-2" />
Opt-out
{m["settings.privacy.opt_out"]()}
</button>
</div>
</div>

View File

@ -6,6 +6,8 @@
import clsx from "clsx";
import Dropdown from "$lib/components/functional/Dropdown.svelte";
import { vertdLoaded } from "$lib/store/index.svelte";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
let vertdCommit = $state<string | null>(null);
let abortController: AbortController | null = null;
@ -55,7 +57,7 @@
class="inline-block -mt-1 mr-2 bg-accent-red p-2 rounded-full overflow-visible"
color="black"
/>
Video conversion
{m["settings.vertd.title"]()}
</h2>
<p
class={clsx("text-sm font-normal", {
@ -64,90 +66,79 @@
"!text-muted": vertdCommit === "loading",
})}
>
status: {vertdCommit
{m["settings.vertd.status"]()} {vertdCommit
? vertdCommit === "loading"
? "loading..."
: `available, commit id ${vertdCommit}`
: "unavailable (is the url right?)"}
? m["settings.vertd.loading"]()
: m["settings.vertd.available"]({ commitId: vertdCommit })
: m["settings.vertd.unavailable"]()}
</p>
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<p class="text-sm text-muted font-normal">
The <code>vertd</code> project is a server wrapper for FFmpeg.
This allows you to convert videos through the convenience of
VERT's web interface, while still being able to harness the power
of your GPU to do it as quickly as possible.
{@html m["settings.vertd.description"]()}
</p>
<p class="text-sm text-muted font-normal">
We host a public instance for your convenience, but it is
quite easy to host your own on your PC or server if you know
what you are doing. You can download the server binaries <a
href={GITHUB_URL_VERTD}
target="_blank">here</a
> - the process of setting this up will become easier in the
future, so stay tuned!
{@html link("vertd_link", m["settings.vertd.hosting_info"](), GITHUB_URL_VERTD)}
</p>
<div class="flex flex-col gap-2">
<p class="text-base font-bold">Instance URL</p>
<p class="text-base font-bold">{m["settings.vertd.instance_url"]()}</p>
<input
type="text"
placeholder="Example: http://localhost:24153"
placeholder={m["settings.vertd.url_placeholder"]()}
bind:value={settings.vertdURL}
/>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">Conversion speed</p>
<p class="text-base font-bold">{m["settings.vertd.conversion_speed"]()}</p>
<p class="text-sm text-muted font-normal">
This describes the tradeoff between speed and
quality. Faster speeds will result in lower quality,
but will get the job done quicker.
{m["settings.vertd.speed_description"]()}
</p>
</div>
<Dropdown
options={[
"Very Slow",
"Slower",
"Slow",
"Medium",
"Fast",
"Ultra Fast",
m["settings.vertd.speeds.very_slow"](),
m["settings.vertd.speeds.slower"](),
m["settings.vertd.speeds.slow"](),
m["settings.vertd.speeds.medium"](),
m["settings.vertd.speeds.fast"](),
m["settings.vertd.speeds.ultra_fast"](),
]}
settingsStyle
selected={(() => {
switch (settings.vertdSpeed) {
case "verySlow":
return "Very Slow";
return m["settings.vertd.speeds.very_slow"]();
case "slower":
return "Slower";
return m["settings.vertd.speeds.slower"]();
case "slow":
return "Slow";
return m["settings.vertd.speeds.slow"]();
case "medium":
return "Medium";
return m["settings.vertd.speeds.medium"]();
case "fast":
return "Fast";
return m["settings.vertd.speeds.fast"]();
case "ultraFast":
return "Ultra Fast";
return m["settings.vertd.speeds.ultra_fast"]();
}
})()}
onselect={(selected) => {
switch (selected) {
case "Very Slow":
case m["settings.vertd.speeds.very_slow"]():
settings.vertdSpeed = "verySlow";
break;
case "Slower":
case m["settings.vertd.speeds.slower"]():
settings.vertdSpeed = "slower";
break;
case "Slow":
case m["settings.vertd.speeds.slow"]():
settings.vertdSpeed = "slow";
break;
case "Medium":
case m["settings.vertd.speeds.medium"]():
settings.vertdSpeed = "medium";
break;
case "Fast":
case m["settings.vertd.speeds.fast"]():
settings.vertdSpeed = "fast";
break;
case "Ultra Fast":
case m["settings.vertd.speeds.ultra_fast"]():
settings.vertdSpeed = "ultraFast";
break;
}

View File

@ -6,6 +6,7 @@ import { parseBlob, selectCover } from "music-metadata";
import { writable } from "svelte/store";
import { addDialog } from "./DialogProvider";
import PQueue from "p-queue";
import { getLocale, setLocale } from "$lib/paraglide/runtime";
class Files {
public files = $state<VertFile[]>([]);
@ -292,3 +293,47 @@ export const vertdLoaded = writable(false);
export const isMobile = writable(false);
export const effects = writable(true);
export const theme = writable<"light" | "dark">("light");
export const locale = writable(getLocale());
export const availableLocales = {
"en": "English",
"uwu": "UwU",
}
export function updateLocale(newLocale: string) {
log(["locale"], `set to ${newLocale}`);
localStorage.setItem("locale", newLocale);
// @ts-expect-error shush
setLocale(newLocale, { reload: false });
// @ts-expect-error shush
locale.set(newLocale);
}
export function link(
tag: string | string[],
text: string,
links: string | string[],
newTab?: boolean | boolean[],
className?: string | string[]
) {
if (!text) return "";
const tags = Array.isArray(tag) ? tag : [tag];
const linksArr = Array.isArray(links) ? links : [links];
const newTabArr = Array.isArray(newTab) ? newTab : [newTab];
const classArr = Array.isArray(className) ? className : [className];
let result = text;
tags.forEach((t, i) => {
const link = linksArr[i] ?? "#";
const target = newTabArr[i] ? 'target="_blank" rel="noopener noreferrer"' : "";
const cls = classArr[i] ? `class="${classArr[i]}"` : "";
const regex = new RegExp(`\\[${t}\\](.*?)\\[\\/${t}\\]`, "g");
result = result.replace(regex, (_, inner) =>
`<a href="${link}" ${target} ${cls} >${inner}</a>`
);
});
return result;
}

View File

@ -1,6 +1,7 @@
import { converters } from "$lib/converters";
import type { Converter } from "$lib/converters/converter.svelte";
import { error } from "$lib/logger";
import { m } from "$lib/paraglide/messages";
import { addToast } from "$lib/store/ToastProvider";
export class VertFile {
@ -75,7 +76,10 @@ export class VertFile {
error(["files"], castedErr.message);
addToast(
"error",
`Error converting file ${this.file.name}: ${castedErr.message || castedErr}`,
m["workers.errors.general"]({
file: this.file.name,
message: castedErr.message || castedErr,
}),
);
this.result = null;
}

View File

@ -113,8 +113,6 @@ const formatToReader = (format: Format): string => {
return "rtf";
case ".rst":
return "rst";
case ".xml":
return "xml";
}
throw new Error(`Unsupported format: ${format}`);

View File

@ -15,11 +15,14 @@
theme,
dropping,
vertdLoaded,
locale,
updateLocale,
} from "$lib/store/index.svelte";
import "$lib/css/app.scss";
import { browser } from "$app/environment";
import { page } from "$app/state";
import { initStores as initAnimStores } from "$lib/animation/index.js";
import { locales, localizeHref } from "$lib/paraglide/runtime";
let { children, data } = $props();
let enablePlausible = $state(false);
@ -68,6 +71,7 @@
theme.set(
(localStorage.getItem("theme") as "light" | "dark") || "light",
);
updateLocale(localStorage.getItem("locale") || "en");
Settings.instance.load();
@ -138,6 +142,7 @@
</svelte:head>
<!-- FIXME: if user resizes between desktop/mobile, highlight of page disappears (only shows on original size) -->
{#key $locale}
<div
class="flex flex-col min-h-screen h-full w-full overflow-x-hidden"
ondrop={dropFiles}
@ -158,6 +163,13 @@
`<slot />` or `{@render ...}` tag missing — inner content will not be rendered
-->
<Layout.PageContent {children} />
<div style="display:none">
{#each locales as locale}
<a href={localizeHref(page.url.pathname, { locale })}
>{locale}</a
>
{/each}
</div>
<Layout.Toasts />
<Layout.Dialogs />
@ -167,6 +179,7 @@
<Navbar.Mobile />
</div>
</div>
{/key}
<!-- Gradients placed here to prevent it overlapping in transitions -->
<Layout.Gradients />

View File

@ -5,6 +5,8 @@
import { vertdLoaded } from "$lib/store/index.svelte";
import clsx from "clsx";
import { AudioLines, BookText, Check, Film, Image } from "lucide-svelte";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
const getSupportedFormats = (name: string) =>
converters
@ -20,6 +22,7 @@
ready: boolean;
formats: string;
icon: typeof Image;
title: string;
};
} = $derived({
Images: {
@ -28,16 +31,19 @@
false,
formats: getSupportedFormats("imagemagick"),
icon: Image,
title: m["upload.cards.images"](),
},
Audio: {
ready: converters.find((c) => c.name === "ffmpeg")?.ready || false,
formats: getSupportedFormats("ffmpeg"),
icon: AudioLines,
title: m["upload.cards.audio"](),
},
Documents: {
ready: converters.find((c) => c.name === "pandoc")?.ready || false,
formats: getSupportedFormats("pandoc"),
icon: BookText,
title: m["upload.cards.documents"](),
},
Video: {
ready:
@ -45,6 +51,7 @@
(false && $vertdLoaded),
formats: getSupportedFormats("vertd"),
icon: Film,
title: m["upload.cards.video"](),
},
});
@ -58,9 +65,10 @@
);
if (formatInfo) {
return `This format can only be converted as ${
formatInfo.fromSupported ? "input (from)" : "output (to)"
}.`;
const direction = formatInfo.fromSupported
? m["upload.tooltip.direction_input"]()
: m["upload.tooltip.direction_output"]();
return m["upload.tooltip.partial_support"]({ direction });
}
return "";
};
@ -75,14 +83,12 @@
<h1
class="text-4xl px-12 md:p-0 md:text-6xl flex-wrap tracking-tight leading-tight md:leading-[72px] mb-4 md:mb-6"
>
The file converter you'll love.
{m["upload.title"]()}
</h1>
<p
class="font-normal px-5 md:p-0 text-lg md:text-xl text-black text-muted dynadark:text-muted"
>
All image, audio, and document processing is done on your
device. Videos are converted on our lightning-fast servers.
No file size limit, no ads, and completely open source.
{m["upload.subtitle"]()}
</p>
</div>
<div class="flex-grow w-full h-72">
@ -94,7 +100,7 @@
<hr />
<div class="mt-10 md:mt-16">
<h2 class="text-center text-4xl">VERT supports...</h2>
<h2 class="text-center text-4xl">{m["upload.cards.title"]()}</h2>
<div class="flex gap-4 mt-8 md:flex-row flex-col">
{#each Object.entries(status) as [key, s]}
@ -111,31 +117,38 @@
>
<Icon size="20" />
</div>
<span>{key}</span>
<span>{s.title}</span>
</div>
<div class="file-category-card-content flex-grow gap-4">
{#if key === "Video"}
<p>
Video uploads to a server for processing by
default, learn how to set it up locally <a
target="_blank"
href="https://github.com/VERT-sh/VERT/wiki/How-to-convert-video-with-VERT"
>here</a
>.
{@html link(
"wiki_link",
m["upload.cards.video_server_processing"](),
"https://github.com/VERT-sh/VERT/wiki/How-to-convert-video-with-VERT",
)}
</p>
{:else}
<p class="flex tems-center justify-center gap-2">
<Check size="20" /> Local fully supported
<Check size="20" />
{m["upload.cards.local_supported"]()}
</p>
{/if}
<p>
<b>Status: </b>
{s.ready ? "ready" : "not ready"}
{@html m["upload.cards.status.text"]({
status: s.ready
? m["upload.cards.status.ready"]()
: m["upload.cards.status.not_ready"](),
})}
</p>
<div>
<span class="flex flex-wrap justify-center">
<b>Supported formats:&nbsp;</b>
<b
>{m[
"upload.cards.supported_formats"
]()}&nbsp;</b
>
{#each s.formats.split(", ") as format, index}
{@const isPartial = format.endsWith("*")}
{@const formatName = isPartial

View File

@ -12,6 +12,7 @@
import { addToast } from "$lib/store/ToastProvider";
import { dev } from "$app/environment";
import { page } from "$app/state";
import { m } from "$lib/paraglide/messages";
// import { dev } from "$app/environment";
// import { page } from "$app/state";
@ -34,19 +35,19 @@
{
name: "nullptr",
github: "https://github.com/not-nullptr",
role: "Lead developer; conversion backend, UI implementation",
role: m["about.credits.roles.lead_developer"](),
avatar: avatarNullptr,
},
{
name: "JovannMC",
github: "https://github.com/JovannMC",
role: "Developer; UI implementation",
role: m["about.credits.roles.developer"](),
avatar: avatarJovannMC,
},
{
name: "Liam",
github: "https://x.com/z2rMC",
role: "Designer; UX, branding, marketing",
role: m["about.credits.roles.designer"](),
avatar: avatarLiam,
},
];
@ -55,13 +56,13 @@
{
name: "azurejelly",
github: "https://github.com/azurejelly",
role: "Maintaining Docker & CI support",
role: m["about.credits.roles.docker_ci"](),
avatar: avatarAzurejelly,
},
{
name: "Realmy",
github: "https://github.com/RealmyTheMan",
role: "Former co-founder & designer",
role: m["about.credits.roles.former_cofounder"](),
avatar: avatarRealmy,
},
];
@ -80,7 +81,7 @@
try {
const response = await fetch(`${GITHUB_API_URL}/contributors`);
if (!response.ok) {
addToast("error", "Error fetching GitHub contributors");
addToast("error", m["about.errors.github_contributors"]());
throw new Error(`HTTP error, status: ${response.status}`);
}
const allContribs = await response.json();
@ -135,7 +136,7 @@
<div class="flex flex-col h-full items-center">
<h1 class="hidden md:block text-[40px] tracking-tight leading-[72px] mb-6">
<InfoIcon size="40" class="inline-block -mt-2 mr-2" />
About
{m["about.title"]()}
</h1>
<div

View File

@ -28,6 +28,7 @@
XIcon,
} from "lucide-svelte";
import { onMount } from "svelte";
import { m } from "$lib/paraglide/messages";
onMount(() => {
// depending on format, select right category and format
@ -120,23 +121,23 @@
<Panel class="p-5 flex flex-col min-w-0 gap-4 relative">
<div class="flex-shrink-0 h-8 w-full flex items-center gap-2">
{#if !converters.length}
<Tooltip text="Unknown file type" position="bottom">
<Tooltip text={m["convert.tooltips.unknown_file"]()} position="bottom">
<FileQuestionIcon size="24" class="flex-shrink-0" />
</Tooltip>
{:else if isAudio}
<Tooltip text="Audio file" position="bottom">
<Tooltip text={m["convert.tooltips.audio_file"]()} position="bottom">
<AudioLines size="24" class="flex-shrink-0" />
</Tooltip>
{:else if isVideo}
<Tooltip text="Video file" position="bottom">
<Tooltip text={m["convert.tooltips.video_file"]()} position="bottom">
<FilmIcon size="24" class="flex-shrink-0" />
</Tooltip>
{:else if isDocument}
<Tooltip text="Document file" position="bottom">
<Tooltip text={m["convert.tooltips.document_file"]()} position="bottom">
<BookText size="24" class="flex-shrink-0" />
</Tooltip>
{:else}
<Tooltip text="Image file" position="bottom">
<Tooltip text={m["convert.tooltips.image_file"]()} position="bottom">
<ImageIcon size="24" class="flex-shrink-0" />
</Tooltip>
{/if}
@ -172,11 +173,10 @@
class="h-full flex flex-col text-center justify-center text-failure"
>
<p class="font-body font-bold">
We can't convert this file.
{m["convert.errors.cant_convert"]()}
</p>
<p class="font-normal">
what are you doing..? you're supposed to run the vertd
server!
{m["convert.errors.vertd_server"]()}
</p>
</div>
{:else}
@ -184,11 +184,10 @@
class="h-full flex flex-col text-center justify-center text-failure"
>
<p class="font-body font-bold">
We can't convert this file.
{m["convert.errors.cant_convert"]()}
</p>
<p class="font-normal">
Only image, video, audio, and document files are
supported
{m["convert.errors.unsupported_format"]()}
</p>
</div>
{/if}
@ -196,10 +195,9 @@
<div
class="h-full flex flex-col text-center justify-center text-failure"
>
<p class="font-body font-bold">We can't convert this file.</p>
<p class="font-body font-bold">{m["convert.errors.cant_convert"]()}</p>
<p class="font-normal">
Could not find the vertd instance to start video conversion.
Are you sure the instance URL is set correctly?
{m["convert.errors.vertd_not_found"]()}
</p>
</div>
{:else}
@ -251,7 +249,7 @@
onselect={(option) => handleSelect(option, file)}
/>
<div class="w-full flex items-center justify-between">
<Tooltip text="Convert this file" position="bottom">
<Tooltip text={m["convert.tooltips.convert_file"]()} position="bottom">
<button
class="btn {$effects
? ''
@ -269,7 +267,7 @@
</button>
</Tooltip>
<Tooltip
text="Download this file"
text={m["convert.tooltips.download_file"]()}
position="bottom"
>
<button

View File

@ -5,6 +5,7 @@
import { files } from "$lib/store/index.svelte";
import { quintOut } from "svelte/easing";
import { blur } from "svelte/transition";
import { m } from "$lib/paraglide/messages";
const images = $derived(
files.files.filter((f) =>
@ -31,9 +32,9 @@
</script>
<div class="mx-auto w-full max-w-[778px] flex flex-col gap-8">
<h1 class="text-5xl text-center">SECRET JPEGIFY!!!</h1>
<h1 class="text-5xl text-center">{m["jpegify.title"]()}</h1>
<p class="text-muted text-center -mt-4 font-normal italic">
(shh... don't tell anyone!)
{m["jpegify.subtitle"]()}
</p>
<Uploader class="w-full h-64" jpegify={true} />
<input
@ -49,7 +50,7 @@
onclick={jpegify}
disabled={processing}
class="btn bg-accent text-black rounded-2xl text-2xl w-full mx-auto"
>JPEGIFY {compressionInverted}%!!!</button
>{m["jpegify.button"]({ compression: compressionInverted })}</button
>
<div class="flex flex-wrap flex-row justify-center gap-4">
{#each images as file, i (file.id)}
@ -89,7 +90,7 @@
disabled={!!!file.result}
class="btn bg-accent text-black rounded-2xl text-2xl w-full mx-auto"
>
Download
{m["jpegify.download"]()}
</button>
<button
onclick={() => {
@ -103,7 +104,7 @@
}}
class="btn border-accent-red border-2 bg-transparent text-black dynadark:text-white rounded-2xl text-2xl w-full mx-auto"
>
Delete
{m["jpegify.delete"]()}
</button>
</div>
</Panel>

View File

@ -6,6 +6,7 @@
import { PUB_PLAUSIBLE_URL } from "$env/static/public";
import { SettingsIcon } from "lucide-svelte";
import { onMount } from "svelte";
import { m } from "$lib/paraglide/messages";
let settings = $state(Settings.Settings.instance.settings);
@ -31,7 +32,7 @@
log(["settings"], "saving settings");
} catch (error) {
log(["settings", "error"], `failed to save settings: ${error}`);
addToast("error", "Failed to save settings!");
addToast("error", m["settings.errors.save_failed"]());
}
});
@ -51,7 +52,7 @@
<div class="flex flex-col h-full items-center">
<h1 class="hidden md:block text-[40px] tracking-tight leading-[72px] mb-6">
<SettingsIcon size="40" class="inline-block -mt-2 mr-2" />
Settings
{m["settings.title"]()}
</h1>
<div

View File

@ -1,3 +1,4 @@
import { paraglideVitePlugin } from "@inlang/paraglide-js";
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig, type PluginOption } from "vite";
import svg from "@poppanator/sveltekit-svg";
@ -6,6 +7,11 @@ import wasm from "vite-plugin-wasm";
export default defineConfig(({ command }) => {
const plugins: PluginOption[] = [
sveltekit(),
paraglideVitePlugin({
project: "./project.inlang",
outdir: "./src/lib/paraglide",
strategy: ["globalVariable", "preferredLanguage", "baseLocale"],
}),
svg({
includePaths: ["./src/lib/assets"],
svgoOptions: {