refactor: migrate from i18next to paraglide
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
Tomáš Mládek 2025-09-27 00:04:24 +02:00
parent f6f096d933
commit 29d96f8451
30 changed files with 470 additions and 214 deletions

3
.gitignore vendored
View file

@ -11,3 +11,6 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Paraglide
src/lib/paraglide

87
messages/en.json Normal file
View file

@ -0,0 +1,87 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"search": "Search",
"tests_audio_description": "Check your stereo channels or surround audio output, verify if your speakers are in phase.",
"tests_audio_label": "Audio",
"tests_av-sync_description": "Check if your audio and video are in sync, and measure the delay.",
"tests_av-sync_label": "Audio/Video Sync",
"tests_card_description": "Test card for your display or projector, check colors, resolution and geometry.",
"tests_card_label": "Card",
"tests_camera_description": "Check whether your webcam or capture device is working, its image quality, resolution and frame rate. Take a snapshot.",
"tests_camera_label": "Camera",
"tests_gamepad_description": "Test your gamepad, check if it's working, all the buttons and joysticks, stick drift, dead zones and calibration.",
"tests_gamepad_label": "Gamepad",
"tests_keyboard_description": "Check if all keys are working and what key codes they send.",
"tests_keyboard_label": "Keyboard",
"tests_microphone_description": "Check if your microphone is working, its quality, volume and noise.",
"tests_microphone_label": "Microphone",
"tests_mouse_description": "Check if your mouse or touch device works properly, if there are dead zones or jitter.",
"tests_mouse_label": "Mouse",
"tests_sensors_description": "See the output of your device's sensors, e.g. GPS, accelerometer, gyroscope, compass, etc.",
"tests_sensors_label": "Sensors",
"tests_internet_description": "Measure your internet speed and latency.",
"tests_internet_label": "Internet speed",
"tests_timer_description": "Check if your high resolution timer is working.",
"tests_timer_label": "High resolution timer",
"category_inputs": "Inputs",
"category_outputs": "Outputs",
"category_audio": "Audio",
"category_video": "Video",
"category_control": "Control",
"category_misc": "Miscellaneous",
"noTestsFound": "No tests found.",
"camera_title": "Camera test",
"camera_device": "Device",
"camera_noCameraFound": "No camera found",
"camera_refresh": "Refresh",
"camera_resolution": "Resolution",
"camera_frameRate": "Frame rate",
"camera_noCameraSelected": "No camera selected",
"camera_takePicture": "Take picture",
"camera_unflipImage": "Unflip image",
"camera_flipImage": "Flip image",
"camera_closeSnapshot": "Close snapshot",
"audio_channel_frontLeft": "Front Left",
"audio_channel_frontCenter": "Front Center",
"audio_channel_frontRight": "Front Right",
"audio_channel_sideLeft": "Side Left",
"audio_channel_sideRight": "Side Right",
"audio_channel_rearLeft": "Rear Left",
"audio_channel_rearRight": "Rear Right",
"audio_channel_lfe": "LFE",
"gamepad_title": "Gamepad & Joystick Tests",
"gamepad_device": "Device",
"gamepad_noGamepadsDetected": "No gamepads detected. (Try pressing a button)",
"gamepad_refresh": "Refresh",
"gamepad_buttons": "Buttons",
"gamepad_axes": "Axes",
"gamepad_history": "History",
"audio_channelTests": "Channel tests",
"audio_stereo": "Stereo",
"audio_surroundAudio": "Surround audio",
"audio_surround51": "5.1 Surround",
"audio_surround71": "7.1 Surround",
"audio_phaseTest": "Phase test",
"audio_frequency": "Frequency",
"audio_inPhase": "In Phase",
"audio_outOfPhase": "Out of Phase",
"audio_stop": "Stop",
"screenInfo_screenResolution": "Screen Resolution",
"screenInfo_windowResolution": "Window Resolution",
"screenInfo_devicePixelRatio": "Device Pixel Ratio",
"audio_channel_left": "Left",
"audio_channel_center": "Center",
"audio_channel_right": "Right",
"keyboard_title": "Keyboard testing",
"keyboard_instruction": "Press a key on the keyboard to see the event object and the key code.",
"keyboard_pressedKeys": "Pressed keys:",
"timer_title": "High resolution timer",
"timer_fps": "FPS",
"timer_restart": "Restart",
"audio_stopCycling": "Stop Cycling",
"audio_cycleThrough": "Cycle through",
"common_back": "Back",
"audio_title": "Audio test",
"avSync_title": "Audio/Video Synchronization",
"internet_title": "Internet speed"
}

View file

@ -16,6 +16,9 @@
"av:render:audio": "cd av-sync && node render-audio.js"
},
"devDependencies": {
"@inlang/paraglide-js": "^2.0.0",
"@inlang/plugin-m-function-matcher": "^2.1.0",
"@inlang/plugin-message-format": "^4.0.0",
"@tsconfig/svelte": "^5.0.4",
"@types/debug": "^4.1.12",
"@types/eslint": "8.56.0",
@ -44,11 +47,9 @@
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tabler/icons-webfont": "^2.47.0",
"debug": "^4.4.0",
"i18next": "^23.16.8",
"lodash": "^4.17.21",
"normalize.css": "^8.0.1",
"svelte": "^5.0.0",
"svelte-i18next": "^2.2.2",
"tslib": "^2.8.1",
"typescript": "^5.7.3",
"vite": "^5.4.14"
@ -57,5 +58,10 @@
"esbuild",
"puppeteer",
"svelte-preprocess"
],
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
}
}

249
pnpm-lock.yaml generated
View file

@ -32,9 +32,6 @@ importers:
debug:
specifier: ^4.4.0
version: 4.4.3
i18next:
specifier: ^23.16.8
version: 23.16.8
lodash:
specifier: ^4.17.21
version: 4.17.21
@ -44,9 +41,6 @@ importers:
svelte:
specifier: ^5.0.0
version: 5.39.6
svelte-i18next:
specifier: ^2.2.2
version: 2.2.2(i18next@23.16.8)(svelte@5.39.6)
tslib:
specifier: ^2.8.1
version: 2.8.1
@ -57,6 +51,15 @@ importers:
specifier: ^5.4.14
version: 5.4.20(@types/node@24.5.2)
devDependencies:
'@inlang/paraglide-js':
specifier: ^2.0.0
version: 2.4.0
'@inlang/plugin-m-function-matcher':
specifier: ^2.1.0
version: 2.1.0
'@inlang/plugin-message-format':
specifier: ^4.0.0
version: 4.0.0
'@tsconfig/svelte':
specifier: ^5.0.4
version: 5.0.5
@ -104,7 +107,7 @@ importers:
version: 22.15.0(typescript@5.9.2)
svelte-check:
specifier: ^4.0.0
version: 4.3.2(svelte@5.39.6)(typescript@5.9.2)
version: 4.3.2(picomatch@4.0.3)(svelte@5.39.6)(typescript@5.9.2)
wait-on:
specifier: ^7.2.0
version: 7.2.0(debug@4.4.3)
@ -304,6 +307,23 @@ packages:
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
deprecated: Use @eslint/object-schema instead
'@inlang/paraglide-js@2.4.0':
resolution: {integrity: sha512-T/m9uoev574/1JrhCnPcgK1xnAwkVMgaDev4LFthnmID8ubX2xjboSGO3IztwXWwO0aJoT1UJr89JCwjbwgnJQ==}
hasBin: true
'@inlang/plugin-m-function-matcher@2.1.0':
resolution: {integrity: sha512-IAbG7rOl+rlTiZY7qj92we6lmII693lVthPtY9bFDkZ/Ig7FPSpae/TfLzqjf2KcR1nDdx1zRpSo6roDPeM85g==}
'@inlang/plugin-message-format@4.0.0':
resolution: {integrity: sha512-zNpLxLTt+bDd3JLXj1ONzo+Q6AOzz2MfcgGo8XB6/bweGhFIndK3GU/q0iU4o7VI4KS1+OHNLpKwFcrAifwERQ==}
'@inlang/recommend-sherlock@0.2.1':
resolution: {integrity: sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==}
'@inlang/sdk@2.4.9':
resolution: {integrity: sha512-cvz/C1rF5WBxzHbEoiBoI6Sz6q6M+TdxfWkEGBYTD77opY8i8WN01prUWXEM87GPF4SZcyIySez9U0Ccm12oFQ==}
engines: {node: '>=18.0.0'}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@ -320,6 +340,13 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@lix-js/sdk@0.4.7':
resolution: {integrity: sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ==}
engines: {node: '>=18'}
'@lix-js/server-protocol-schema@0.1.1':
resolution: {integrity: sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -459,6 +486,13 @@ packages:
'@sideway/pinpoint@2.0.0':
resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
'@sinclair/typebox@0.31.28':
resolution: {integrity: sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==}
'@sqlite.org/sqlite-wasm@3.48.0-build4':
resolution: {integrity: sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==}
hasBin: true
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
@ -640,6 +674,9 @@ packages:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
array-timsort@1.0.3:
resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
array-union@2.1.0:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'}
@ -765,10 +802,18 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@11.1.0:
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
engines: {node: '>=16'}
commander@12.1.0:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
comment-json@4.2.5:
resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==}
engines: {node: '>= 6'}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@ -777,10 +822,17 @@ packages:
engines: {node: ^14.13.0 || >=16.0.0}
hasBin: true
consola@3.4.0:
resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==}
engines: {node: ^14.18.0 || >=16.10.0}
cookie@0.6.0:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
cosmiconfig@9.0.0:
resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
engines: {node: '>=14'}
@ -816,6 +868,14 @@ packages:
supports-color:
optional: true
dedent@1.5.1:
resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==}
peerDependencies:
babel-plugin-macros: ^3.1.0
peerDependenciesMeta:
babel-plugin-macros:
optional: true
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@ -1018,6 +1078,11 @@ packages:
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
engines: {node: ^10.12.0 || >=12.0.0}
flat@6.0.1:
resolution: {integrity: sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw==}
engines: {node: '>=18'}
hasBin: true
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
@ -1096,6 +1161,10 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
has-own-prop@2.0.0:
resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==}
engines: {node: '>=8'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
@ -1116,8 +1185,9 @@ packages:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
i18next@23.16.8:
resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==}
human-id@4.1.1:
resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==}
hasBin: true
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
@ -1180,6 +1250,9 @@ packages:
joi@17.13.3:
resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==}
js-sha256@0.11.1:
resolution: {integrity: sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -1199,6 +1272,11 @@ packages:
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
hasBin: true
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@ -1209,6 +1287,10 @@ packages:
known-css-properties@0.35.0:
resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==}
kysely@0.27.6:
resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==}
engines: {node: '>=14.0.0'}
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@ -1360,6 +1442,10 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
postcss-load-config@3.1.4:
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
engines: {node: '>= 10'}
@ -1442,6 +1528,10 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
repeat-string@1.6.1:
resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
engines: {node: '>=0.10'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@ -1525,6 +1615,11 @@ packages:
spawn-command@0.0.2:
resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==}
sqlite-wasm-kysely@0.3.0:
resolution: {integrity: sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==}
peerDependencies:
kysely: '*'
streamx@2.23.0:
resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==}
@ -1565,12 +1660,6 @@ packages:
svelte:
optional: true
svelte-i18next@2.2.2:
resolution: {integrity: sha512-IpJDZCH5cCgKfHQHgiLmGT4j9HCdg4fqsP3oP2deLu8PxmNj0Ui6khMiDoxAxedAiYEhr0xendv2xqh3Rq+uQQ==}
peerDependencies:
i18next: '*'
svelte: '*'
svelte@5.39.6:
resolution: {integrity: sha512-bOJXmuwLNaoqPCTWO8mPu/fwxI5peGE5Efe7oo6Cakpz/G60vsnVF6mxbGODaxMUFUKEnjm6XOwHEqOht6cbvw==}
engines: {node: '>=18'}
@ -1630,6 +1719,10 @@ packages:
undici-types@7.12.0:
resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==}
unplugin@2.3.10:
resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==}
engines: {node: '>=18.12.0'}
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@ -1639,6 +1732,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
vite@5.4.20:
resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==}
engines: {node: ^18.0.0 || >=20.0.0}
@ -1683,6 +1780,9 @@ packages:
engines: {node: '>=12.0.0'}
hasBin: true
webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@ -1866,6 +1966,42 @@ snapshots:
'@humanwhocodes/object-schema@2.0.3': {}
'@inlang/paraglide-js@2.4.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.3.10
urlpattern-polyfill: 10.0.0
transitivePeerDependencies:
- babel-plugin-macros
'@inlang/plugin-m-function-matcher@2.1.0':
dependencies:
'@inlang/sdk': 2.4.9
transitivePeerDependencies:
- babel-plugin-macros
'@inlang/plugin-message-format@4.0.0':
dependencies:
flat: 6.0.1
'@inlang/recommend-sherlock@0.2.1':
dependencies:
comment-json: 4.2.5
'@inlang/sdk@2.4.9':
dependencies:
'@lix-js/sdk': 0.4.7
'@sinclair/typebox': 0.31.28
kysely: 0.27.6
sqlite-wasm-kysely: 0.3.0(kysely@0.27.6)
uuid: 10.0.0
transitivePeerDependencies:
- babel-plugin-macros
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@ -1885,6 +2021,20 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@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.1
kysely: 0.27.6
sqlite-wasm-kysely: 0.3.0(kysely@0.27.6)
uuid: 10.0.0
transitivePeerDependencies:
- babel-plugin-macros
'@lix-js/server-protocol-schema@0.1.1': {}
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -1988,6 +2138,10 @@ snapshots:
'@sideway/pinpoint@2.0.0': {}
'@sinclair/typebox@0.31.28': {}
'@sqlite.org/sqlite-wasm@3.48.0-build4': {}
'@standard-schema/spec@1.0.0': {}
'@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)':
@ -2198,6 +2352,8 @@ snapshots:
aria-query@5.3.2: {}
array-timsort@1.0.3: {}
array-union@2.1.0: {}
ast-types@0.13.4:
@ -2320,8 +2476,18 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
commander@11.1.0: {}
commander@12.1.0: {}
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
concat-map@0.0.1: {}
concurrently@8.2.2:
@ -2336,8 +2502,12 @@ snapshots:
tree-kill: 1.2.2
yargs: 17.7.2
consola@3.4.0: {}
cookie@0.6.0: {}
core-util-is@1.0.3: {}
cosmiconfig@9.0.0(typescript@5.9.2):
dependencies:
env-paths: 2.2.1
@ -2365,6 +2535,8 @@ snapshots:
dependencies:
ms: 2.1.3
dedent@1.5.1: {}
deep-is@0.1.4: {}
deepmerge@4.3.1: {}
@ -2602,7 +2774,9 @@ snapshots:
dependencies:
pend: 1.2.0
fdir@6.5.0: {}
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
file-entry-cache@6.0.1:
dependencies:
@ -2623,6 +2797,8 @@ snapshots:
keyv: 4.5.4
rimraf: 3.0.2
flat@6.0.1: {}
flatted@3.3.3: {}
follow-redirects@1.15.11(debug@4.4.3):
@ -2712,6 +2888,8 @@ snapshots:
has-flag@4.0.0: {}
has-own-prop@2.0.0: {}
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
@ -2736,9 +2914,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
i18next@23.16.8:
dependencies:
'@babel/runtime': 7.28.4
human-id@4.1.1: {}
ieee754@1.2.1: {}
@ -2790,6 +2966,8 @@ snapshots:
'@sideway/formula': 3.0.1
'@sideway/pinpoint': 2.0.0
js-sha256@0.11.1: {}
js-tokens@4.0.0: {}
js-yaml@4.1.0:
@ -2804,6 +2982,8 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {}
json5@2.2.3: {}
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@ -2812,6 +2992,8 @@ snapshots:
known-css-properties@0.35.0: {}
kysely@0.27.6: {}
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
@ -2944,6 +3126,8 @@ snapshots:
picomatch@2.3.1: {}
picomatch@4.0.3: {}
postcss-load-config@3.1.4(postcss@8.5.6):
dependencies:
lilconfig: 2.1.0
@ -3035,6 +3219,8 @@ snapshots:
readdirp@4.1.2: {}
repeat-string@1.6.1: {}
require-directory@2.1.1: {}
resolve-from@4.0.0: {}
@ -3127,6 +3313,11 @@ snapshots:
spawn-command@0.0.2: {}
sqlite-wasm-kysely@0.3.0(kysely@0.27.6):
dependencies:
'@sqlite.org/sqlite-wasm': 3.48.0-build4
kysely: 0.27.6
streamx@2.23.0:
dependencies:
events-universal: 1.0.1
@ -3155,11 +3346,11 @@ snapshots:
dependencies:
has-flag: 4.0.0
svelte-check@4.3.2(svelte@5.39.6)(typescript@5.9.2):
svelte-check@4.3.2(picomatch@4.0.3)(svelte@5.39.6)(typescript@5.9.2):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
chokidar: 4.0.3
fdir: 6.5.0
fdir: 6.5.0(picomatch@4.0.3)
picocolors: 1.1.1
sade: 1.8.1
svelte: 5.39.6
@ -3177,11 +3368,6 @@ snapshots:
optionalDependencies:
svelte: 5.39.6
svelte-i18next@2.2.2(i18next@23.16.8)(svelte@5.39.6):
dependencies:
i18next: 23.16.8
svelte: 5.39.6
svelte@5.39.6:
dependencies:
'@jridgewell/remapping': 2.3.5
@ -3258,6 +3444,13 @@ snapshots:
undici-types@7.12.0:
optional: true
unplugin@2.3.10:
dependencies:
'@jridgewell/remapping': 2.3.5
acorn: 8.15.0
picomatch: 4.0.3
webpack-virtual-modules: 0.6.2
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
@ -3266,6 +3459,8 @@ snapshots:
util-deprecate@1.0.2: {}
uuid@10.0.0: {}
vite@5.4.20(@types/node@24.5.2):
dependencies:
esbuild: 0.21.5
@ -3289,6 +3484,8 @@ snapshots:
transitivePeerDependencies:
- debug
webpack-virtual-modules@0.6.2: {}
which@2.0.2:
dependencies:
isexe: 2.0.0

View file

@ -0,0 +1 @@
bkDf3wpqa4gWMSsmqk

View file

@ -0,0 +1,12 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"modules": [
"./node_modules/@inlang/plugin-message-format/dist/index.js",
"./node_modules/@inlang/plugin-m-function-matcher/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json"
},
"baseLocale": "en",
"locales": ["en"]
}

View file

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

13
src/hooks.server.ts Normal file
View file

@ -0,0 +1,13 @@
import type { Handle } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
const handleParaglide: Handle = ({ event, resolve }) =>
paraglideMiddleware(event.request, ({ request, locale }) => {
event.request = request;
return resolve(event, {
transformPageChunk: ({ html }) => html.replace('%paraglide.lang%', locale)
});
});
export const handle: Handle = handleParaglide;

3
src/hooks.ts Normal file
View file

@ -0,0 +1,3 @@
import { deLocalizeUrl } from '$lib/paraglide/runtime';
export const reroute = (request) => deLocalizeUrl(request.url).pathname;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
let screenResolution = $state('... x ...');
let windowResolution = $state('');
@ -27,16 +27,16 @@
<div class="info">
<div class="resolution">
<div class="title">{$i18n.t('Screen Resolution')}</div>
<div class="title">{m.screenInfo_screenResolution()}</div>
<div class="value">{screenResolution}</div>
{#if windowResolution && windowResolution !== screenResolution}
<div class="window" transition:fade>
<div class="title">{$i18n.t('Window Resolution')}</div>
<div class="title">{m.screenInfo_windowResolution()}</div>
<div class="value">{windowResolution}</div>
</div>
{/if}
{#if dpr !== '1'}
<div class="dpr">{$i18n.t('Device Pixel Ratio')}: {dpr}</div>
<div class="dpr">{m.screenInfo_devicePixelRatio()}: {dpr}</div>
{/if}
</div>
</div>

View file

@ -1,14 +0,0 @@
import i18next from 'i18next';
import { createI18nStore } from 'svelte-i18next';
import enTranslation from './locales/en.json';
i18next.init({
lng: 'en',
resources: {
en: {
translation: enTranslation
}
}
});
export const i18n = createI18nStore(i18next);

View file

@ -1,48 +0,0 @@
{
"tests": {
"audio": {
"label": "Audio",
"description": "Check your stereo channels or surround audio output, verify if your speakers are in phase."
},
"av-sync": {
"label": "Audio/Video Sync",
"description": "Check if your audio and video are in sync, and measure the delay."
},
"card": {
"label": "Card",
"description": "Test card for your display or projector, check colors, resolution and geometry."
},
"camera": {
"label": "Camera",
"description": "Check whether your webcam or capture device is working, its image quality, resolution and frame rate. Take a snapshot."
},
"gamepad": {
"label": "Gamepad",
"description": "Test your gamepad, check if it's working, all the buttons and joysticks, stick drift, dead zones and calibration."
},
"keyboard": {
"label": "Keyboard",
"description": "Check if all keys are working and what key codes they send."
},
"microphone": {
"label": "Microphone",
"description": "Check if your microphone is working, its quality, volume and noise."
},
"mouse": {
"label": "Mouse",
"description": "Check if your mouse or touch device works properly, if there are dead zones or jitter."
},
"sensors": {
"label": "Sensors",
"description": "See the output of your device's sensors, e.g. GPS, accelerometer, gyroscope, compass, etc."
},
"internet": {
"label": "Internet speed",
"description": "Measure your internet speed, ping and jitter."
},
"timer": {
"label": "High resolution timer",
"description": "Display a millisecond resolution timer on screen, useful for measuring video pipeline latency."
}
}
}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
interface Props {
children?: import('svelte').Snippet;
}
@ -7,7 +7,7 @@
let { children }: Props = $props();
</script>
<a href=".." class="hide-idle"><i class="ti ti-arrow-back"></i> {$i18n.t('Back')}</a>
<a href=".." class="hide-idle"><i class="ti ti-arrow-back"></i> {m.common_back()}</a>
{@render children?.()}
<style>

View file

@ -2,7 +2,7 @@
import TestCard from '$lib/TestCard.svelte';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
interface Props {
children?: import('svelte').Snippet;
}
@ -12,7 +12,7 @@
<TestCard bg on:focus={() => goto('/card')} />
<main class:sub={!page.data.root}>
<a href=".." class="button button-back"><i class="ti ti-arrow-back"></i>{$i18n.t('Back')}</a>
<a href=".." class="button button-back"><i class="ti ti-arrow-back"></i>{m.common_back()}</a>
{@render children?.()}
</main>

View file

@ -2,16 +2,15 @@
import { run } from 'svelte/legacy';
import { version } from '../../../package.json';
import { i18n } from '$lib/i18n';
import type { Snapshot } from '@sveltejs/kit';
import { fade } from 'svelte/transition';
const buildDate = import.meta.env.VITE_BUILD_DATE || '???';
import { m } from '$lib/paraglide/messages.js';
let search = $state('');
type Entry = {
id: string;
label: string;
icon: string;
};
@ -25,12 +24,10 @@
let superCategories = [
{
id: 'inputs',
label: 'Inputs',
icon: 'ti-arrow-down-to-arc'
},
{
id: 'outputs',
label: 'Outputs',
icon: 'ti-arrow-down-from-arc'
}
] as const satisfies Entry[];
@ -38,96 +35,81 @@
let categories = [
{
id: 'audio',
label: 'Audio',
icon: 'ti-volume'
},
{
id: 'video',
label: 'Video',
icon: 'ti-video'
},
{
id: 'control',
label: 'Control',
icon: 'ti-hand-finger'
},
{
id: 'misc',
label: 'Miscellaneous',
icon: 'ti-circle-plus'
}
] as const satisfies Entry[];
const tests: Test[] = [
const tests = [
{
id: 'card',
label: 'Test Card',
icon: 'ti-device-desktop',
categories: ['outputs', 'video']
},
{
id: 'audio',
label: 'Audio',
icon: 'ti-volume',
categories: ['outputs', 'audio']
},
{
id: 'av-sync',
label: 'AV Sync',
icon: 'ti-time-duration-off',
categories: ['outputs', 'video', 'audio']
},
{
id: 'keyboard',
label: 'Keyboard',
icon: 'ti-keyboard',
categories: ['inputs', 'control']
},
{
id: 'mouse',
label: 'Mouse',
icon: 'ti-mouse',
categories: ['inputs', 'control']
},
{
id: 'gamepad',
label: 'Gamepad',
icon: 'ti-device-gamepad',
categories: ['inputs', 'control']
},
{
id: 'camera',
label: 'Camera',
icon: 'ti-camera',
categories: ['inputs', 'video']
},
{
id: 'microphone',
label: 'Microphone',
icon: 'ti-microphone',
categories: ['inputs', 'audio'],
disabled: true
},
{
id: 'sensors',
label: 'Sensors',
icon: 'ti-cpu-2',
categories: ['inputs', 'misc'],
disabled: true
},
{
id: 'internet',
label: 'Internet speed',
icon: 'ti-world',
categories: ['inputs', 'outputs', 'misc']
},
{
id: 'timer',
label: 'High resolution timer',
icon: 'ti-alarm',
categories: ['video']
}
];
] as const satisfies Test[];
let categoriesToIcons = $derived.by(() => {
const map = new Map<string, string>();
@ -137,7 +119,7 @@
return map;
});
let filteredTests: Test[] = $state(tests);
let filteredTests: Array<(typeof tests)[number]> = $state(tests);
let filteredCategories: Category[] = $state([]);
function doSearch(search: string) {
@ -146,9 +128,9 @@
const searchValue = search.toLocaleLowerCase();
return (
test.label.includes(searchValue) ||
test.id.includes(searchValue) ||
$i18n.t(`tests.${test.id}.description`).includes(searchValue)
m[`tests_${test.id}_label`]().includes(searchValue) ||
m[`tests_${test.id}_description`]().includes(searchValue)
);
});
}
@ -166,10 +148,13 @@
let nonEmptyCategories = $derived(
categories.filter((category) => {
const categoryTests = filteredTests.filter((test) => test.categories.includes(category.id));
const categoryTests = filteredTests.filter((test) =>
(test.categories as readonly Category[]).includes(category.id)
);
return categoryTests.some(
(test) =>
!filteredCategories.length || filteredCategories.every((f) => test.categories.includes(f))
!filteredCategories.length ||
filteredCategories.every((f) => (test.categories as readonly Category[]).includes(f))
);
})
);
@ -188,7 +173,7 @@
<nav>
<!-- svelte-ignore a11y_autofocus -->
<input type="search" placeholder={$i18n.t('Search')} bind:value={search} autofocus />
<input type="search" placeholder={m.search()} bind:value={search} autofocus />
<div class="options">
{#each superCategories as category}
@ -198,7 +183,7 @@
class="super"
>
<i class="ti {category.icon}"></i>
{$i18n.t(category.label)}
{m[`category_${category.id}`]()}
</button>
{/each}
<div class="separator"></div>
@ -208,37 +193,37 @@
class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
>
<i class="ti {category.icon}"></i>
{$i18n.t(category.label)}
{m[`category_${category.id}`]()}
</button>
{/each}
</div>
<div class="tests">
{#each nonEmptyCategories as category}
{#if tests.filter((test) => test.categories.includes(category.id)).length > 0}
<h2 transition:fade={{ duration: 200 }}>{$i18n.t(category.label)}</h2>
{#each filteredTests.filter((test) => test.categories.includes(category.id) && filteredCategories.every( (f) => test.categories.includes(f) )) as test}
{#if tests.filter( (test) => (test.categories as readonly Category[]).includes(category.id) ).length > 0}
<h2 transition:fade={{ duration: 200 }}>{m[`category_${category.id}`]()}</h2>
{#each filteredTests.filter((test) => (test.categories as readonly Category[]).includes(category.id) && filteredCategories.every( (f) => (test.categories as readonly Category[]).includes(f) )) as test}
<a
class="test"
href={test.id}
class:disabled={test.disabled}
class:disabled={(test as Test).disabled}
transition:fade={{ duration: 200 }}
>
<i class="ti {test.icon}"></i>
<div class="label">
<span class="name">{$i18n.t(test.label)}</span>
<span class="name">{m[`tests_${test.id}_label`]()}</span>
{#each test.categories as category}
<span class="category"><i class="ti {categoriesToIcons.get(category)}"></i></span>
{/each}
</div>
<div class="description">
{$i18n.t(`tests.${test.id}.description`)}
{m[`tests_${test.id}_description`]()}
</div>
</a>
{/each}
{/if}
{:else}
<p>
{$i18n.t('No tests found.')}
{m.noTestsFound()}
</p>
{/each}
</div>

View file

@ -6,7 +6,7 @@
let { children }: Props = $props();
let channelsEl: HTMLDivElement = $state();
let channelsEl: HTMLDivElement | undefined = $state();
</script>
<div class="channels" bind:this={channelsEl}>

View file

@ -6,20 +6,20 @@
import rearLeftUrl from '@assets/audio/5.1/Rear_Left.mp3';
import rearRightUrl from '@assets/audio/5.1/Rear_Right.mp3';
import LfeUrl from '@assets/audio/5.1/LFE_Noise.mp3';
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
</script>
<div class="row">
<Speaker src={frontLeftUrl} left>{$i18n.t('Front Left')}</Speaker>
<Speaker src={frontLeftUrl} left>{m.audio_channel_frontLeft()}</Speaker>
<div class="center">
<Speaker src={frontCenterUrl} center>{$i18n.t('Front Center')}</Speaker>
<Speaker src={frontCenterUrl} center>{m.audio_channel_frontCenter()}</Speaker>
</div>
<Speaker src={frontRightUrl} right>{$i18n.t('Front Right')}</Speaker>
<Speaker src={frontRightUrl} right>{m.audio_channel_frontRight()}</Speaker>
</div>
<div class="row">
<Speaker src={rearLeftUrl} left>{$i18n.t('Rear Left')}</Speaker>
<Speaker src={rearRightUrl} right>{$i18n.t('Rear Right')}</Speaker>
<Speaker src={rearLeftUrl} left>{m.audio_channel_rearLeft()}</Speaker>
<Speaker src={rearRightUrl} right>{m.audio_channel_rearRight()}</Speaker>
</div>
<Speaker src={LfeUrl} lfe>{$i18n.t('LFE')}</Speaker>
<Speaker src={LfeUrl} lfe>{m.audio_channel_lfe()}</Speaker>
<div class="label">5.1</div>

View file

@ -8,25 +8,25 @@
import rearLeftUrl from '@assets/audio/7.1/Rear_Left.mp3';
import rearRightUrl from '@assets/audio/7.1/Rear_Right.mp3';
import LfeUrl from '@assets/audio/7.1/LFE_Noise.mp3';
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
</script>
<div class="row">
<Speaker src={frontLeftUrl} left>{$i18n.t('Front Left')}</Speaker>
<Speaker src={frontLeftUrl} left>{m.audio_channel_frontLeft()}</Speaker>
<div class="center">
<Speaker src={frontCenterUrl} center>{$i18n.t('Front Center')}</Speaker>
<Speaker src={frontCenterUrl} center>{m.audio_channel_frontCenter()}</Speaker>
</div>
<Speaker src={frontRightUrl} right>{$i18n.t('Front Right')}</Speaker>
<Speaker src={frontRightUrl} right>{m.audio_channel_frontRight()}</Speaker>
</div>
<div class="row">
<Speaker src={sideLeftUrl} left>{$i18n.t('Side Left')}</Speaker>
<Speaker src={sideRightUrl} right>{$i18n.t('Side Right')}</Speaker>
<Speaker src={sideLeftUrl} left>{m.audio_channel_sideLeft()}</Speaker>
<Speaker src={sideRightUrl} right>{m.audio_channel_sideRight()}</Speaker>
</div>
<div class="row">
<Speaker src={rearLeftUrl} left>{$i18n.t('Rear Left')}</Speaker>
<Speaker src={rearRightUrl} right>{$i18n.t('Rear Right')}</Speaker>
<Speaker src={rearLeftUrl} left>{m.audio_channel_rearLeft()}</Speaker>
<Speaker src={rearRightUrl} right>{m.audio_channel_rearRight()}</Speaker>
</div>
<Speaker src={LfeUrl} lfe>{$i18n.t('LFE')}</Speaker>
<Speaker src={LfeUrl} lfe>{m.audio_channel_lfe()}</Speaker>
<div class="label">7.1</div>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
interface Props {
element: HTMLElement;
@ -51,8 +51,8 @@
<button onclick={onClick}>
<i class="ti ti-refresh"></i>
{#if cycling}
{$i18n.t('Stop Cycling')}
{m.audio_stopCycling()}
{:else}
{$i18n.t('Cycle through')}
{m.audio_cycleThrough()}
{/if}
</button>

View file

@ -4,16 +4,16 @@
import rightUrl from '@assets/audio/stereo/Right.mp3';
import Speaker from './speaker.svelte';
import CycleButton from './cycle-button.svelte';
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
let speakersEl: HTMLElement = $state();
let speakersEl: HTMLElement | undefined = $state();
</script>
<div class="test">
<div class="speakers" bind:this={speakersEl}>
<Speaker src={leftUrl} left inline>{$i18n.t('Left')}</Speaker>
<Speaker src={centerUrl} center inline>{$i18n.t('Center')}</Speaker>
<Speaker src={rightUrl} right inline>{$i18n.t('Right')}</Speaker>
<Speaker src={leftUrl} left inline>{m.audio_channel_left()}</Speaker>
<Speaker src={centerUrl} center inline>{m.audio_channel_center()}</Speaker>
<Speaker src={rightUrl} right inline>{m.audio_channel_right()}</Speaker>
</div>
<CycleButton element={speakersEl} />
</div>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
interface Props {
children?: import('svelte').Snippet;
}
@ -7,5 +7,5 @@
let { children }: Props = $props();
</script>
<h2><i class="ti ti-volume"></i> {$i18n.t('Audio test')}</h2>
<h2><i class="ti ti-volume"></i> {m.audio_title()}</h2>
{@render children?.()}

View file

@ -1,23 +1,23 @@
<script lang="ts">
import StereoTest from './(channels)/stereo-test.svelte';
import PhaseTest from './phase.svelte';
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
</script>
<article>
<h3>{$i18n.t('Channel tests')}</h3>
<h4>{$i18n.t('Stereo')}</h4>
<h3>{m.audio_channelTests()}</h3>
<h4>{m.audio_stereo()}</h4>
<section>
<StereoTest />
</section>
<h4>{$i18n.t('Surround audio')}</h4>
<h4>{m.audio_surroundAudio()}</h4>
<section>
<ul>
<li><a class="button" href="channels-5.1">{$i18n.t('5.1 Surround')}</a></li>
<li><a class="button" href="channels-7.1">{$i18n.t('7.1 Surround')}</a></li>
<li><a class="button" href="channels-5.1">{m.audio_surround51()}</a></li>
<li><a class="button" href="channels-7.1">{m.audio_surround71()}</a></li>
</ul>
</section>
<h3>{$i18n.t('Phase test')}</h3>
<h3>{m.audio_phaseTest()}</h3>
<PhaseTest />
</article>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
let frequency = $state(60);
let playing = $state(false);
@ -54,13 +54,13 @@
<div class="test">
<label>
{$i18n.t('Frequency')}
{m.audio_frequency()}
<input type="number" bind:value={frequency} min="20" max="20000" disabled={playing} />Hz
</label>
<div class="controls">
<button onclick={() => start('inPhase')}>{$i18n.t('In Phase')}</button>
<button onclick={() => start('outOfPhase')}>{$i18n.t('Out of Phase')}</button>
<button class="stop" onclick={stop} disabled={!playing}>{$i18n.t('Stop')}</button>
<button onclick={() => start('inPhase')}>{m.audio_inPhase()}</button>
<button onclick={() => start('outOfPhase')}>{m.audio_outOfPhase()}</button>
<button class="stop" onclick={stop} disabled={!playing}>{m.audio_stop()}</button>
</div>
</div>

View file

@ -1,10 +1,10 @@
<script lang="ts">
import videoUrl from '@assets/avsync.webm';
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
let paused = $state(true);
</script>
<h2><i class="ti ti-time-duration-off"></i> {$i18n.t('Audio/Video Synchronization')}</h2>
<h2><i class="ti ti-time-duration-off"></i> {m.avSync_title()}</h2>
<!-- svelte-ignore a11y_media_has_caption -->
<video
class:playing={!paused}

View file

@ -4,10 +4,10 @@
import { onDestroy, onMount } from 'svelte';
import { browser } from '$app/environment';
import debug from 'debug';
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
const dbg = debug('app:camera');
let video: HTMLVideoElement = $state();
let video: HTMLVideoElement | undefined = $state();
let devices: MediaDeviceInfo[] = $state([]);
let currentDevice: string | undefined = $state();
@ -29,7 +29,7 @@
onMount(() => {
refreshDevices();
video.addEventListener('playing', () => {
video?.addEventListener('playing', () => {
if (browser && video?.srcObject instanceof MediaStream) {
deviceInfo = {
resolution: `${video.videoWidth}x${video.videoHeight}`,
@ -66,6 +66,7 @@
}
})
.then((stream) => {
if (!video) return;
video.srcObject = stream;
refreshDevices();
});
@ -73,6 +74,7 @@
});
async function takeSnapshot() {
if (!video) return;
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
@ -87,26 +89,26 @@
}
</script>
<h2><i class="ti ti-camera"></i> {$i18n.t('Camera test')}</h2>
<h2><i class="ti ti-camera"></i> {m.camera_title()}</h2>
<div class="controls">
<label>
{$i18n.t('Device')}
{m.camera_device()}
<select bind:value={currentDevice} disabled={!devices.length}>
{#each devices as device}
<option value={device.deviceId}>{device.label || '???'}</option>
{:else}
<option>{$i18n.t('No camera found')}</option>
<option>{m.camera_noCameraFound()}</option>
{/each}
</select>
</label>
<button onclick={refreshDevices}>
<i class="ti ti-refresh"></i>
{$i18n.t('Refresh')}
{m.camera_refresh()}
</button>
<div class="separator"></div>
<label>
{$i18n.t('Resolution')}
{m.camera_resolution()}
<select bind:value={requestResolution}>
<option value="auto">Auto</option>
<option value={[4096, 2160]}>4096x2160</option>
@ -118,7 +120,7 @@
</select>
</label>
<label>
{$i18n.t('Frame rate')}
{m.camera_frameRate()}
<select bind:value={requestFramerate}>
<option value="auto">Auto</option>
<option value={120}>120 fps</option>
@ -138,35 +140,37 @@
<!-- svelte-ignore a11y_missing_attribute -->
<!--suppress HtmlRequiredAltAttribute -->
<img src={snapshot} />
<button onclick={() => (snapshot = undefined)}><i class="ti ti-x"></i></button>
<button onclick={() => (snapshot = undefined)} aria-label={m.camera_closeSnapshot()}
><i class="ti ti-x"></i></button
>
{/if}
</div>
<footer>
{#if !currentDevice}
<span class="subdued">{$i18n.t('No camera selected')}</span>
<span class="subdued">{m.camera_noCameraSelected()}</span>
{:else}
<ul>
{#key currentDevice}
<li>
{$i18n.t('Resolution')}: <strong>{deviceInfo.resolution || '???'}</strong>
{m.camera_resolution()}: <strong>{deviceInfo.resolution || '???'}</strong>
</li>
<li>
{$i18n.t('Frame rate')}: <strong>{deviceInfo.frameRate || '???'}</strong>
{m.camera_frameRate()}: <strong>{deviceInfo.frameRate || '???'}</strong>
</li>
{/key}
</ul>
<div class="controls">
<button onclick={takeSnapshot}>
<i class="ti ti-camera"></i>
{$i18n.t('Take picture')}
{m.camera_takePicture()}
</button>
<button onclick={() => (flipped = !flipped)}>
<i class="ti ti-flip-vertical"></i>
{#if flipped}
{$i18n.t('Unflip image')}
{m.camera_unflipImage()}
{:else}
{$i18n.t('Flip image')}
{m.camera_flipImage()}
{/if}
</button>
</div>

View file

@ -4,7 +4,7 @@
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import debug from 'debug';
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
const dbg = debug('app:camera');
@ -101,27 +101,27 @@
});
</script>
<h2><i class="ti ti-device-gamepad"></i> {$i18n.t('Gamepad & Joystick Tests')}</h2>
<h2><i class="ti ti-device-gamepad"></i> {m.gamepad_title()}</h2>
<div class="controls">
<label>
{$i18n.t('Device')}
{m.gamepad_device()}
<select disabled={!gamepads.length}>
{#each gamepads as gamepad}
<option value={gamepad.index}>{gamepad.id}</option>
{:else}
<option>{$i18n.t('No gamepads detected. (Try pressing a button)')}</option>
<option>{m.gamepad_noGamepadsDetected()}</option>
{/each}
</select>
</label>
<button onclick={refreshGamepads}>
<i class="ti ti-refresh"></i>
{$i18n.t('Refresh')}
{m.gamepad_refresh()}
</button>
</div>
{#if currentGamepad}
<section>
<h3>{$i18n.t('Buttons')}</h3>
<h3>{m.gamepad_buttons()}</h3>
<ul class="buttons">
{#each buttons as button, i}
<li class:pressed={button.pressed}>{i}</li>
@ -129,7 +129,7 @@
</ul>
</section>
<section>
<h3>{$i18n.t('Axes')}</h3>
<h3>{m.gamepad_axes()}</h3>
<div class="axes">
{#each axes as axis, i (i)}
<div class="axis">
@ -139,7 +139,7 @@
<span>{axis.toFixed(2)}</span>
</div>
<details>
<summary>{$i18n.t('History')}</summary>
<summary>{m.gamepad_history()}</summary>
<canvas width="512" height="128" data-axis={i}></canvas>
</details>
</div>

View file

@ -1,8 +1,8 @@
<script>
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
</script>
<h2><i class="ti ti-world"></i> {$i18n.t('Internet speed')}</h2>
<h2><i class="ti ti-world"></i> {m.internet_title()}</h2>
<div class="test">
<iframe src="//openspeedtest.com/speedtest" title="OpenSpeedTest Embed"></iframe>

View file

@ -1,9 +1,9 @@
<script lang="ts">
import { onMount } from 'svelte';
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
let key: string = $state();
let code: string = $state();
let key: string | undefined = $state();
let code: string | undefined = $state();
let pressedKeys: string[] = $state([]);
onMount(() => {
document.addEventListener('keydown', (event) => {
@ -15,8 +15,8 @@
});
</script>
<h2>{$i18n.t('Keyboard testing')}</h2>
<p>{$i18n.t('Press a key on the keyboard to see the event object and the key code.')}</p>
<h2>{m.keyboard_title()}</h2>
<p>{m.keyboard_instruction()}</p>
<div class="current">
{#if key}
<span>{key}</span>
@ -26,7 +26,7 @@
{/if}
</div>
<p>{$i18n.t('Pressed keys:')}</p>
<p>{m.keyboard_pressedKeys()}</p>
<ul>
{#each pressedKeys as key}
<li>{key}</li>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { i18n } from '$lib/i18n';
import { m } from '$lib/paraglide/messages';
let time = $state(0);
let fps = 0;
@ -36,12 +36,12 @@
});
</script>
<h2><i class="ti ti-alarm"></i> {$i18n.t('High resolution timer')}</h2>
<h2><i class="ti ti-alarm"></i> {m.timer_title()}</h2>
<div class="display">
<div class="time">{time}</div>
<div class="fps">{displayFps} {$i18n.t('FPS')}</div>
<div class="fps">{displayFps} {m.timer_fps()}</div>
</div>
<button onclick={restart}>{$i18n.t('Restart')}</button>
<button onclick={restart}>{m.timer_restart()}</button>
<style>
div,

View file

@ -1,9 +1,16 @@
import { paraglideVitePlugin } from '@inlang/paraglide-js';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import * as path from 'path';
export default defineConfig({
plugins: [sveltekit()],
plugins: [
sveltekit(),
paraglideVitePlugin({
project: './project.inlang',
outdir: './src/lib/paraglide'
})
],
resolve: {
alias: {
'@assets': path.join(__dirname, 'assets/generated')