Compare commits

...

12 Commits

Author SHA1 Message Date
Maya d89a343eed
fix: open graph tags
maybe this'll work on twitter now
2025-10-14 20:33:05 +03:00
nullptr 92fa929d2a
feat: keep videos for debugging (#130)
* feat: keep videos for debugging

* fix: remove unnecessary $inspect
2025-10-14 18:08:33 +01:00
Maya 704e693511
Merge branch 'pr/129' 2025-10-14 19:14:55 +03:00
Maya 8d38007461
Merge branch 'pr/128' 2025-10-14 19:14:26 +03:00
Danny Davila 17e024d763
chore: update japanese language 2025-10-13 05:22:37 +09:00
Danny Davila e2221e460a
chore: add japanese language 2025-10-13 05:14:10 +09:00
Danny Davila 97ecf9e520
updated spanish translations 2025-10-13 04:37:35 +09:00
DROF4 45b9158140
Update index.svelte.ts
added tr: "Türkçe" for Turhish Language Support
2025-10-12 18:08:08 +03:00
DROF4 b9d94a3f82
Update settings.json (added tr)
Added "tr" for Turkish Language Support
2025-10-12 18:06:50 +03:00
DROF4 44a96313b9
Update tr.json
Final checks and edits for the Turkish translation.
2025-10-12 18:02:14 +03:00
DROF4 35929d824b
Update tr.json
All sections have been translated into Turkish.
2025-10-12 17:52:12 +03:00
DROF4 65ffb1a3c6
Create tr.json 2025-10-12 16:07:29 +03:00
22 changed files with 1061 additions and 160 deletions

View File

@ -79,6 +79,11 @@
"errors": {
"cant_convert": "We can't convert this file.",
"vertd_server": "what are you doing..? you're supposed to run the vertd server!",
"vertd_generic_body": "An error occurred whilst whilst trying convert your video. Would you like to submit this video to the developers to help fix this bug? Only your video file will be sent. No identifiers will be uploaded.",
"vertd_generic_title": "Video conversion error",
"vertd_generic_yes": "Submit video",
"vertd_generic_no": "Don't submit",
"vertd_failed_to_keep": "Failed to keep the video on the server: {error}",
"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?",
"worker_downloading": "The {type} converter is currently being initialized, please wait a few moments.",
@ -167,7 +172,8 @@
"loading_cache": "Loading...",
"total_size": "Total Size",
"files_cached_label": "Files Cached",
"cache_cleared": "Cache cleared successfully!"
"cache_cleared": "Cache cleared successfully!",
"cache_clear_error": "Failed to clear cache."
},
"language": {
"title": "Language",

View File

@ -31,7 +31,11 @@
"status": {
"text": "<b>Estado:</b> {status}",
"ready": "listo",
"not_ready": "no listo"
"not_ready": "no listo",
"not_initialized": "no inicializado",
"downloading": "descargando...",
"initializing": "inicializando...",
"unknown": "estado desconocido"
},
"supported_formats": "Formatos soportados:"
},
@ -43,6 +47,12 @@
}
},
"convert": {
"external_warning": {
"title": "Advertencia del servidor externo",
"text": "Si eliges convertir a un formato de video, esos archivos se cargarán en un servidor externo para convertirlos. ¿Quieres continuar?",
"yes": "Sí",
"no": "No"
},
"panel": {
"convert_all": "Convertir todo",
"download_all": "Comprimir todo",
@ -70,7 +80,13 @@
"cant_convert": "No podemos convertir este archivo.",
"vertd_server": "¿Qué estás haciendo..? ¡Debes ejecutar el servidor de vertd!",
"unsupported_format": "Solo aceptamos imágenes, vídeos, audios y documentos.",
"vertd_not_found": "No se encontró la instancia de vertd para iniciar la conversión de vídeos. ¿Estás seguro de que la URL es correcta?"
"vertd_not_found": "No se encontró la instancia de vertd para iniciar la conversión de vídeos. ¿Estás seguro de que la URL es correcta?",
"worker_downloading": "El convertidor {type} se está inicializando actualmente, espere unos momentos.",
"worker_error": "El convertidor {type} tuvo un error durante la inicialización, inténtelo nuevamente más tarde.",
"worker_timeout": "El convertidor {type} está tardando más de lo esperado en inicializarse. Espere unos momentos más o actualice la página.",
"audio": "audio",
"doc": "documento",
"image": "imagen"
}
},
"settings": {
@ -91,9 +107,20 @@
},
"conversion": {
"title": "Conversión",
"advanced_settings": "Configuraciones avanzadas",
"filename_format": "Formato del nombre de archivo",
"filename_description": "Esto va a determinar el nombre del archivo al ser descargado <b>sin incluir la extensión</b>. Puedes poner las siguientes plantillas en el formato, las cuales serán reemplazadas con la información que les corresponde: <b>%name%</b> para el nombre original, <b>%extension%</b> para la extensión original del archivo y <b>%date%</b> para la fecha de cuando el archivo fue convertido.",
"placeholder": "VERT_%name%",
"default_format": "Formato de conversión predeterminado",
"default_format_description": "Esto cambiará el formato predeterminado seleccionado cuando subes un archivo de este tipo.",
"default_format_image": "Imágenes",
"default_format_video": "Vídeos",
"default_format_audio": "Audio",
"default_format_document": "Documentos",
"metadata": "Metadatos del archivo",
"metadata_description": "Esto cambia si los metadatos (EXIF, información de la canción, etc.) del archivo original se conservan en los archivos convertidos.",
"keep": "Mantener",
"remove": "Eliminar",
"quality": "Calidad de la conversión",
"quality_description": "Esto cambia la calidad por defecto de los archivos convertidos (en su categoría). Valores más altos pueden resultar en tiempos de conversión y tamaños de archivo más largos.",
"quality_video": "Esto cambia la calidad por defecto de los vídeos convertidos. Valores más altos pueden resultar en tiempos de conversión y tamaños de archivo más largos.",
@ -120,14 +147,27 @@
"medium": "Medio",
"fast": "Rápido",
"ultra_fast": "Súper rápido"
}
},
"auto_instance": "Automático (recomendado)",
"eu_instance": "Falkenstein, Alemania",
"us_instance": "Washington, EE. UU.",
"custom_instance": "Personalizado"
},
"privacy": {
"title": "Privacidad",
"plausible_title": "Analíticas de Plausible",
"plausible_description": "Usamos [plausible_link]Plausible[/plausible_link], una herramienta de analíticas orientada a la privacidad para recopilar estadísticas completamente anónimas. Toda la información que recopilamos es anonimizada y agregada, y en ningún momento se envía ni se almacena información que permita identificarte. Puedes ver las estadísticas [analytics_link]aquí[/analytics_link] y excluirte de ellas a continuación:",
"opt_in": "Participar",
"opt_out": "No participar"
"opt_out": "No participar",
"cache_title": "Administración de caché",
"cache_description": "Guardamos en caché los archivos del convertidor en su navegador para que no tenga que volver a descargarlos cada vez, mejorando el rendimiento y reduciendo el uso de datos.",
"refresh_cache": "Actualizar caché",
"clear_cache": "Borrar caché",
"files_cached": "{size} ({count} archivos)",
"loading_cache": "Cargando...",
"total_size": "Tamaño total",
"files_cached_label": "Archivos en caché",
"cache_cleared": "¡Caché borrada exitosamente!"
},
"language": {
"title": "Lenguaje",
@ -189,6 +229,7 @@
"workers": {
"errors": {
"general": "Ocurrió un error mientras se convertía {file}: {message}",
"cancel": "Error al cancelar la conversión para {file}: {message}",
"magick": "Ocurrió un error en el módulo de Magick, la conversión de imágenes puede que no funcione correctamente.",
"ffmpeg": "No se pudo cargar FFmpeg, algunas funciones podrían no funcionar.",
"no_audio": "No se encontró una pista de audio.",

246
messages/ja.json Normal file
View File

@ -0,0 +1,246 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"navbar": {
"upload": "アップロード",
"convert": "変換",
"settings": "設定",
"about": "について",
"toggle_theme": "テーマを切り替える"
},
"footer": {
"copyright": "© {year} VERT.",
"source_code": "ソースコード",
"discord_server": "Discordサーバー"
},
"upload": {
"title": "きっと気に入るファイル変換ツール。",
"subtitle": "すべての画像・音声・ドキュメント処理はデバイス上で行われます。動画は超高速サーバーで変換されます。ファイルサイズ制限なし、広告なし、完全オープンソース。",
"uploader": {
"text": "ドロップまたはクリックして{action}",
"convert": "変換",
"jpegify": "JPEG化"
},
"cards": {
"title": "VERTがサポートしている形式",
"images": "画像",
"audio": "音声",
"documents": "ドキュメント",
"video": "動画",
"video_server_processing": "サーバー対応",
"local_supported": "ローカル対応",
"status": {
"text": "<b>ステータス:</b> {status}",
"ready": "準備完了",
"not_ready": "未準備",
"not_initialized": "未初期化",
"downloading": "ダウンロード中...",
"initializing": "初期化中...",
"unknown": "不明なステータス"
},
"supported_formats": "対応フォーマット:"
},
"tooltip": {
"partial_support": "このフォーマットは{direction}としてのみ変換可能です。",
"direction_input": "入力(変換元)",
"direction_output": "出力(変換先)",
"video_server_processing": "動画はデフォルトでサーバーにアップロードされて処理されます。ローカルで設定する方法はこちら。"
}
},
"convert": {
"external_warning": {
"title": "外部サーバーの警告",
"text": "動画フォーマットへの変換を選択すると、ファイルは外部サーバーにアップロードされて変換されます。続行しますか?",
"yes": "はい",
"no": "いいえ"
},
"panel": {
"convert_all": "すべて変換",
"download_all": "すべてを.zipでダウンロード",
"remove_all": "すべてのファイルを削除",
"set_all_to": "すべてを設定",
"na": "該当なし"
},
"dropdown": {
"audio": "音声",
"video": "動画",
"doc": "ドキュメント",
"image": "画像",
"placeholder": "フォーマットを検索"
},
"tooltips": {
"unknown_file": "不明なファイルタイプ",
"audio_file": "音声ファイル",
"video_file": "動画ファイル",
"document_file": "ドキュメントファイル",
"image_file": "画像ファイル",
"convert_file": "このファイルを変換",
"download_file": "このファイルをダウンロード"
},
"errors": {
"cant_convert": "このファイルを変換できません。",
"vertd_server": "何してるの..? vertdサーバーを起動する必要があります",
"unsupported_format": "画像、動画、音声、ドキュメントのみ対応しています",
"vertd_not_found": "動画変換を開始するためのvertdインスタンスが見つかりません。URLが正しいか確認してください。",
"worker_downloading": "{type}コンバーターを初期化中です。少々お待ちください。",
"worker_error": "{type}コンバーターの初期化中にエラーが発生しました。後でもう一度お試しください。",
"worker_timeout": "{type}コンバーターの初期化に予想以上の時間がかかっています。もう少しお待ちいただくか、ページを更新してください。",
"audio": "音声",
"doc": "ドキュメント",
"image": "画像"
}
},
"settings": {
"title": "設定",
"errors": {
"save_failed": "設定の保存に失敗しました!"
},
"appearance": {
"title": "外観",
"brightness_theme": "明るさテーマ",
"brightness_description": "まぶしい昼間か、静かな夜か?",
"light": "ライト",
"dark": "ダーク",
"effect_settings": "エフェクト設定",
"effect_description": "派手な効果にしますか?それとも静的な体験にしますか?",
"enable": "有効",
"disable": "無効"
},
"conversion": {
"title": "変換",
"advanced_settings": "詳細設定",
"filename_format": "ファイル名フォーマット",
"filename_description": "これはダウンロード時のファイル名を決定します(拡張子を除く)。以下のテンプレートを使用できます:<b>%name%</b>(元のファイル名)、<b>%extension%</b>(元の拡張子)、<b>%date%</b>(変換日時)。",
"placeholder": "VERT_%name%",
"default_format": "デフォルト変換フォーマット",
"default_format_description": "このファイルタイプをアップロードしたときに自動で選択される形式を変更します。",
"default_format_image": "画像",
"default_format_video": "動画",
"default_format_audio": "音声",
"default_format_document": "ドキュメント",
"metadata": "ファイルメタデータ",
"metadata_description": "変換後のファイルに元のメタデータEXIF、曲情報などを保持するかどうかを変更します。",
"keep": "保持",
"remove": "削除",
"quality": "変換品質",
"quality_description": "出力ファイルの品質を変更します。値が高いほど処理時間とファイルサイズが増加します。",
"quality_video": "動画変換の品質を変更します。高品質ほど変換時間とサイズが増加します。",
"quality_audio": "音声kbps",
"quality_images": "画像(%)",
"rate": "サンプリングレートHz"
},
"vertd": {
"title": "動画変換",
"status": "ステータス:",
"loading": "読み込み中...",
"available": "利用可能コミットID {commitId}",
"unavailable": "利用不可URLが正しいですか",
"description": "<code>vertd</code>プロジェクトはFFmpegのサーバーラッパーです。これにより、GPUの性能を活かして高速に変換しつつ、VERTのウェブインターフェイスから簡単に動画を変換できます。",
"hosting_info": "私たちは利便性のために公開インスタンスをホストしていますが、自分のPCやサーバーでも簡単にホストできます。バイナリは[vertd_link]こちら[/vertd_link]からダウンロードできます。今後さらにセットアップが簡単になる予定です!",
"instance": "インスタンス",
"url_placeholder": "例: http://localhost:24153",
"conversion_speed": "変換速度",
"speed_description": "速度と品質のバランスを設定します。高速化すると品質が低下しますが、処理は速くなります。",
"speeds": {
"very_slow": "非常に遅い",
"slower": "かなり遅い",
"slow": "遅い",
"medium": "普通",
"fast": "速い",
"ultra_fast": "超高速"
},
"auto_instance": "自動(推奨)",
"eu_instance": "ドイツ・ファルケンシュタイン",
"us_instance": "アメリカ・ワシントン",
"custom_instance": "カスタム"
},
"privacy": {
"title": "プライバシーとデータ",
"plausible_title": "Plausible解析",
"plausible_description": "私たちはプライバシー重視の解析ツール[plausible_link]Plausible[/plausible_link]を使用しています。すべてのデータは匿名化・集計され、個人情報は一切収集・保存されません。統計情報は[analytics_link]こちら[/analytics_link]で確認でき、以下でオプトアウト可能です。",
"opt_in": "参加する",
"opt_out": "参加しない",
"cache_title": "キャッシュ管理",
"cache_description": "コンバーターファイルをブラウザにキャッシュして再ダウンロードを防ぎ、パフォーマンスを向上させます。",
"refresh_cache": "キャッシュを更新",
"clear_cache": "キャッシュをクリア",
"files_cached": "{size}{count}ファイル)",
"loading_cache": "読み込み中...",
"total_size": "合計サイズ",
"files_cached_label": "キャッシュ済みファイル",
"cache_cleared": "キャッシュが正常にクリアされました!"
},
"language": {
"title": "言語",
"description": "VERTインターフェイスの表示言語を選択してください。"
}
},
"about": {
"title": "について",
"why": {
"title": "なぜVERT",
"description": "<b>従来のファイルコンバーターにはいつもがっかりしてきました。</b>見た目が悪く、広告だらけで、そして何より遅い。私たちはそれらの問題をすべて解決するためにVERTを作りました。<br/><br/>動画以外のファイルは完全にデバイス上で変換されるため、サーバーとのやり取りによる遅延もなく、あなたのファイルを覗き見ることもありません。<br/><br/>動画は超高速RTX 4000 Adaサーバーで処理され、変換しなかった場合は1時間以内に削除されます。変換された動画も1時間またはダウンロード完了後に削除されます。"
},
"sponsors": {
"title": "スポンサー",
"description": "私たちを支援したい場合は、[discord_link]Discord[/discord_link]サーバーで開発者に連絡するか、以下のメールアドレスまでご連絡ください。",
"email_copied": "メールアドレスをコピーしました!"
},
"resources": {
"title": "リソース",
"discord": "Discord",
"source": "ソース",
"email": "メール"
},
"donate": {
"title": "VERTを支援する",
"description": "あなたの支援でVERTの維持と改善を続けられます。",
"one_time": "一度きり",
"monthly": "毎月",
"custom": "カスタム",
"pay_now": "今すぐ支払う",
"donate_amount": "${amount} USDを寄付",
"thank_you": "ご支援ありがとうございます!",
"payment_failed": "支払いに失敗しました: {message}{period} 請求は行われていません。",
"donation_error": "寄付の処理中にエラーが発生しました。後でもう一度お試しください。",
"payment_error": "支払い情報の取得中にエラーが発生しました。後でもう一度お試しください。"
},
"credits": {
"title": "クレジット",
"contact_team": "開発チームに連絡したい場合は、「リソース」カードに記載されたメールをご利用ください。",
"notable_contributors": "特筆すべき貢献者",
"notable_description": "VERTに大きく貢献してくださった方々に感謝します。",
"github_contributors": "GitHubの貢献者",
"github_description": "多くの方々に[jpegify_link]感謝[/jpegify_link]します![github_link]あなたも参加してみませんか?[/github_link]",
"no_contributors": "まだ誰も貢献していないようです… [contribute_link]最初の貢献者になりましょう![/contribute_link]",
"libraries": "ライブラリ",
"libraries_description": "長年にわたり優れたライブラリを提供してくれているFFmpeg音声・動画、ImageMagick画像、Pandocドキュメントに感謝します。VERTはこれらに依存して動作しています。",
"roles": {
"lead_developer": "リード開発者変換バックエンド、UI実装",
"developer": "開発者UI実装",
"designer": "デザイナーUX、ブランディング、マーケティング",
"docker_ci": "DockerとCIの保守担当",
"former_cofounder": "元共同創設者・デザイナー"
}
},
"errors": {
"github_contributors": "GitHub貢献者の取得エラー"
}
},
"workers": {
"errors": {
"general": "{file}の変換エラー:{message}",
"cancel": "{file}の変換キャンセルエラー:{message}",
"magick": "Magickワーカーでエラーが発生しました。画像変換が正常に動作しない可能性があります。",
"ffmpeg": "ffmpegの読み込みエラー。一部の機能が動作しない可能性があります。",
"no_audio": "音声ストリームが見つかりません。",
"invalid_rate": "無効なサンプリングレートが指定されました: {rate}Hz"
}
},
"jpegify": {
"title": "秘密のJPEGIFY!!!",
"subtitle": "(しっ…誰にも言わないで!)",
"button": "JPEGIFY {compression}%!!!",
"download": "ダウンロード",
"delete": "削除"
}
}

247
messages/tr.json Normal file
View File

@ -0,0 +1,247 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"navbar": {
"upload": "Yükle",
"convert": "Dönüştür",
"settings": "Ayarlar",
"about": "Hakkımızda",
"toggle_theme": "Temayı değiştir"
},
"footer": {
"copyright": "© {year} VERT.",
"source_code": "Kaynak kodu",
"discord_server": "Discord sunucusu"
},
"upload": {
"title": "Sevdiğiniz dosya dönüştürücü.",
"subtitle": "Tüm görüntü, ses ve belge işlemleri cihazınızda gerçekleştirilir. Videolar, ışık hızındaki sunucularımızda dönüştürülür. Dosya boyutu sınırı ve reklam yoktur. Tamamen açık kaynaklıdır.",
"uploader": {
"text": "{action} için sürükleyip bırakın veya dosya seçin",
"convert": "dönüştür",
"jpegify": "jpegify"
},
"cards": {
"title": "VERT'in desteklediği formatlar...",
"images": "Görsel",
"audio": "Ses",
"documents": "Belge",
"video": "Video",
"video_server_processing": "Sunucuda gerçekleşir",
"local_supported": "Lokalde gerçekleşir",
"status": {
"text": "<b>Durum:</b> {status}",
"ready": "hazır",
"not_ready": "hazır değil",
"not_initialized": "başlatılmamış",
"downloading": "indiriliyor...",
"initializing": "başlatılıyor...",
"unknown": "bilinmeyen durum"
},
"supported_formats": "Desteklenen formatlar:"
},
"tooltip": {
"partial_support": "Bu format yalnızca şu şekilde dönüştürülebilir: {direction}.",
"direction_input": "kaynak",
"direction_output": ıktı",
"video_server_processing": "Videolar varsayılan olarak işlenmek üzere sunucuya yüklenir. Yerel olarak nasıl ayarlayacağınızı buradan öğrenebilirsiniz."
}
},
"convert": {
"external_warning": {
"title": "Harici sunucu uyarısı",
"text": "Video formatına dönüştürmeyi seçerseniz, bu dosyalar dönüştürülmek üzere harici bir sunucuya yüklenecektir. Devam etmek istiyor musunuz?",
"yes": "Evet",
"no": "Hayır"
},
"panel": {
"convert_all": "Tümünü dönüştür",
"download_all": "Tümünü .zip olarak indir",
"remove_all": "Tüm dosyaları kaldır",
"set_all_to": "Tümünü ayarla",
"na": "N/A"
},
"dropdown": {
"audio": "Ses",
"video": "Video",
"doc": "Belge",
"image": "Görsel",
"placeholder": "Format ara"
},
"tooltips": {
"unknown_file": "Bilinmeyen dosya türü",
"audio_file": "Ses dosyası",
"video_file": "Video dosyası",
"document_file": "Belge dosyası",
"image_file": "Görsel dosyası",
"convert_file": "Bu dosyayı dönüştür",
"download_file": "Bu dosyayı indir"
},
"errors": {
"cant_convert": "Bu dosyayı dönüştüremiyoruz.",
"vertd_server": "Ne yapıyorsun..? vertd sunucusunu çalıştırman gerekiyordu!",
"unsupported_format": "Yalnızca görüntü, video, ses ve belge dosyaları desteklenir.",
"vertd_not_found": "Video dönüştürme işlemini başlatmak için vertd örneği bulunamadı. Sunucu URLsinin doğru ayarlandığından emin misiniz?",
"worker_downloading": "{type} dönüştürme işlemi şu anda başlatılıyor, lütfen birkaç saniye bekleyin.",
"worker_error": "{type} dönüştürme işlemi başlatılırken bir hata oluştu, lütfen daha sonra tekrar deneyin.",
"worker_timeout": "{type} dönüştürme işlemi beklenenden daha uzun sürüyor, lütfen biraz daha bekleyin veya sayfayı yenileyin.",
"audio": "ses",
"doc": "belge",
"image": "görsel"
}
},
"settings": {
"title": "Ayarlar",
"errors": {
"save_failed": "Ayarlar kaydedilirken hata oluştu!"
},
"appearance": {
"title": "Görünüm",
"brightness_theme": "Tema seçimi",
"brightness_description": "Güneşli bir gün mü istersiniz, yoksa sessiz ve yalnız bir gece mi?",
"light": "Açık",
"dark": "Koyu",
"effect_settings": "Efekt ayarları",
"effect_description": "Süslü efektler mi istersiniz, yoksa daha sade bir deneyim mi?",
"enable": "Etkinleştir",
"disable": "Devre dışı bırak"
},
"conversion": {
"title": "Dönüştürme",
"advanced_settings": "Gelişmiş ayarlar",
"filename_format": "Dosya adı formatı",
"filename_description": "Bu ayar, <b>dosya uzantısını etkilemeden</b>indirilen dosyanın adını belirleyecektir. Aşağıdaki şablonları formata ekleyebilirsiniz, bunlar ilgili bilgilerle değiştirilecektir: orijinal dosya adı için <b>%name%</b>, orijinal dosya uzantısı için <b>%extension%</b> ve dosyanın dönüştürüldüğü tarihin tarih için <b>%date%</b>.",
"placeholder": "VERT_%name%",
"default_format": "Varsayılan dönüştürme formatı",
"default_format_description": "Bu ayar, bu dosya türünde bir dosya yüklediğinizde seçili olan varsayılan formatı değiştirecektir.",
"default_format_image": "Görsel",
"default_format_video": "Video",
"default_format_audio": "Ses",
"default_format_document": "Belge",
"metadata": "Dosya metadata",
"metadata_description": "Bu ayar, orijinal dosyadaki meta verilerin (EXIF, şarkı bilgileri vb.) dönüştürülen dosyalarda korunup korunmayacağını değiştirir.",
"keep": "Sakla",
"remove": "Kaldır",
"quality": "Dönüştürme kalitesi",
"quality_description": "Bu, dönüştürülen dosyaların (kendi kategorisinde) varsayılan çıktı kalitesini değiştirir. Yüksek değerler, uzun dönüştürme sürelerine ve büyük dosya boyutuna neden olabilir.",
"quality_video": "Bu, dönüştürülen videoların varsayılan çıktı kalitesini değiştirir. Yüksek değerler, uzun dönüştürme sürelerine ve büyük dosya boyutuna neden olabilir.",
"quality_audio": "Ses (kbps)",
"quality_images": "Görsel (%)",
"rate": "Örnekleme oranı (Hz)"
},
"vertd": {
"title": "Video dönüştürme",
"status": "durum:",
"loading": "yükleniyor...",
"available": "uygun, işlem no: {commitId}",
"unavailable": "uygun değil (url doğru mu?)",
"description": "<code>vertd</code> projesi, FFmpeg için bir sunucu sarmalayıcısıdır (server wrapper). Bu ayar, VERT'in web arayüzünün kullanım kolaylığı ile videoları dönüştürmenize olanak sağlarken, ekran kartınızın gücünden yararlanarak işlemi mümkün olan en hızlı şekilde yapmanızı sağlar.",
"hosting_info": "Kolaylık sağlamasıısından herkese açık bir dönüştürücü sunuyoruz, ancak kendi bilgisayarınızda veya sunucunuzda kendi dönüştürücünüzü kurmak da oldukça kolaydır. Sunucu binary dosyalarını [vertd_link]buradan[/vertd_link] indirebilirsiniz. Kurulum işlemini gelecekte daha kolay hale getirmeye çalışıyoruz, bu nedenle bizi takip etmeyi unutmayın!",
"instance": "Sunucu",
"url_placeholder": "Örneğin: http://localhost:24153",
"conversion_speed": "Dönüştürme hızı",
"speed_description": "Bu ayar, hız ve kalite arasındaki dengeyi belirlemenizi sağlar. Yüksek hızlar, düşük kaliteye neden olur ancak işlem daha hızlı tamamlanır.",
"speeds": {
"very_slow": "En Yavaş",
"slower": "Daha Yavaş",
"slow": "Yavaş",
"medium": "Orta",
"fast": "Hızlı",
"ultra_fast": "En Hızlı"
},
"auto_instance": "Otomatik (önerilen)",
"eu_instance": "Falkenstein, Germany",
"us_instance": "Washington, USA",
"custom_instance": "Özel"
},
"privacy": {
"title": "Gizlilik & kişisel veriler",
"plausible_title": "Plausible analytics",
"plausible_description": "Tamamen anonim istatistikler toplamak için gizliliğe odaklı bir analiz aracı olan [plausible_link]Plausible[/plausible_link]ı kullanıyoruz. Tüm veriler anonimleştirilmiş ve birleştirilmiş şekilde işlenir; hiçbir kişisel veya tanımlanabilir bilgi gönderilmez ya da saklanmaz. Analitik verilerini [analytics_link]buradan[/analytics_link] görüntüleyebilir ve aşağıdan devre dışı bırakmayı seçebilirsiniz.",
"opt_in": "Etkinleştir",
"opt_out": "Devre dışı bırak",
"cache_title": "Önbellek yönetimi",
"cache_description": "Dönüştürücü dosyalarını tarayıcınızda önbelleğe alırız, böylece her seferinde yeniden indirmenize gerek kalmaz, performans artar ve veri kullanımı azalır.",
"refresh_cache": "Önbelleği Yenile",
"clear_cache": "Önbelleği Temizle",
"files_cached": "{size} ({count} dosya)",
"loading_cache": "Yükleniyor...",
"total_size": "Toplam Boyut",
"files_cached_label": "Önbelleğe Alınan Dosyalar",
"cache_cleared": "Önbellek başarıyla temizlendi."
},
"language": {
"title": "Dil",
"description": "VERT arayüzü için tercih ettiğiniz dili seçin."
}
},
"about": {
"title": "Hakkımızda",
"why": {
"title": "Neden VERT?",
"description": "<b>Dosya dönüştürücüler bizi her zaman hayal kırıklığına uğratmıştır. </b> Çoğu dönüştürücü site, kötü ve reklamlarla dolu arayüze sahiptir ve en önemlisi yavaştır. Tüm bu sorunları ve daha fazlasını çözen bir alternatif oluşturarak bu sorunu sonsuza kadar çözmeye karar verdik. <br/><br/>Video dışındaki tüm dosyalar tamamen cihazınızda dönüştürülür; bu, sunucuya dosya yükleme ve sunucudan dosya indirme sırasında gecikme olmaması ve dönüştürdüğünüz dosyaların asla başka biri tarafından görüntülenememesi anlamına gelir. <br/><br/>Video dosyaları, ışık hızındaki RTX 4000 Ada sunucumuza yüklenir. Videolarınızı dönüştürseniz de dönüştürmeseniz de bir saat sonra sunucularımızdan silinir. Video dönüştürme işlemi gerçekleştirirseniz, bir saat içinde dönüştürülmüş dosyayı indirebilirsiniz. Dosya daha sonra sunucumuzdan silinir."
},
"sponsors": {
"title": "Sponsorlar",
"description": "Bizi desteklemek ister misiniz? [discord_link]Discord[/discord_link] sunucumuzda bir geliştiriciyle iletişime geçin veya şu adrese e-posta gönderin:",
"email_copied": "E-posta kopyalandı!"
},
"resources": {
"title": "Bağlantılar",
"discord": "Discord",
"source": "GitHub",
"email": "E-posta"
},
"donate": {
"title": "VERT'e bağış yapın",
"description": "Sizin desteğinizle VERT'i çalıştırmaya ve geliştirmeye devam edebiliriz.",
"one_time": "Tek seferlik",
"monthly": "Aylık",
"custom": "Özel",
"pay_now": "Ödeme yap",
"donate_amount": "${amount} USD Bağış Yap",
"thank_you": "Bağışınız için teşekkür ederiz!",
"payment_failed": "Ödeme başarısız: {message}{period} Kartınızdan para çekilmedi.",
"donation_error": "Bağışınız işlenirken bir hata oluştu. Lütfen daha sonra tekrar deneyin.",
"payment_error": "Ödeme bilgileri alınırken hata oluştu. Lütfen daha sonra tekrar deneyin."
},
"credits": {
"title": "Katkıda bulunanlar",
"contact_team": "Geliştirme ekibiyle iletişime geçmek isterseniz, \"Bağlantılar\" kısmında bulunan e-posta adresini kullanabilirsiniz.",
"notable_contributors": "Önemli katılımcılar",
"notable_description": "VERT'e sağladıkları büyük katkılardan dolayı bu kişilere teşekkür ederiz.",
"github_contributors": "GitHub katılımcıları",
"github_description": "Yardımcı olan herkese çok [jpegify_link]teşekkürler[/jpegify_link]! [github_link]Sen de yardım etmek ister misin?[/github_link]",
"no_contributors": "Henüz kimse katkıda bulunmamış gibi görünüyor... [contribute_link]ilk katkıda bulunan sen ol![/contribute_link]",
"libraries": "Kütüphaneler",
"libraries_description": "Bu mükemmel kütüphaneleri yıllardır geliştirdikleri için FFmpeg (ses, video), ImageMagick (görseller) ve Pandoc (belgeler)'a çok teşekkür ederiz. VERT, dönüştürme işlemlerini için bu kütüphaneleri kullanmaktadır.",
"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": "GitHub katılımcılarını yüklerken hata oluştu"
}
},
"workers": {
"errors": {
"general": "{dosya} dönüştürülürken hata oluştu: {message}",
"cancel": "{dosya} için dönüştürme işlemi iptal edilirken hata oluştu: {message}",
"magick": "Magick işlemi sırasında hata oluştu, görsel dönüştürme işlemi beklendiği gibi çalışmayabilir.",
"ffmpeg": "ffmpeg yüklenirken hata oluştu, bazı özellikler çalışmayabilir.",
"no_audio": "Ses akışı bulunamadı.",
"invalid_rate": "Geçersiz örnekleme hızı: {hız}Hz"
}
},
"jpegify": {
"title": "GİZLİ JPEGIFY!!!",
"subtitle": "(şşş... kimseye söyleme!)",
"button": "JPEGIFY {compression}%!!!",
"download": "İndir",
"delete": "Sil"
}
}

View File

@ -1,7 +1,7 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"baseLocale": "en",
"locales": ["en", "es", "fr", "de", "hr"],
"locales": ["en", "es", "fr", "de", "hr", "tr", "ja"],
"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"

View File

@ -0,0 +1,73 @@
<script lang="ts" module>
export interface VertdErrorProps {
jobId: string;
auth: string;
}
</script>
<script lang="ts">
import { vertdFetch } from "$lib/converters/vertd.svelte";
import { m } from "$lib/paraglide/messages";
import { ToastManager, type ToastProps } from "$lib/toast/index.svelte";
const toast: ToastProps<VertdErrorProps> = $props();
let submitting = $state(false);
export const title = "An error occurred";
const remove = () => {
ToastManager.remove(toast.id);
};
const submit = async () => {
submitting = true;
try {
await submitInner();
} catch (e) {}
submitting = false;
};
const submitInner = async () => {
try {
await vertdFetch(
"/api/keep",
{
method: "POST",
},
{
token: toast.additional.auth,
id: toast.additional.jobId,
},
);
} catch (e) {
ToastManager.add({
type: "error",
message: m["convert.errors.vertd_failed_to_keep"]({
error: (e as Error).message || e || "Unknown error",
}),
});
}
ToastManager.remove(toast.id);
};
</script>
<div class="flex flex-col gap-4">
<p class="text-black">{m["convert.errors.vertd_generic_body"]()}</p>
<div class="flex gap-4">
<button
onclick={submit}
class="btn rounded-lg h-fit py-2 w-full bg-accent-red-alt text-white"
disabled={submitting}
>{m["convert.errors.vertd_generic_yes"]()}</button
>
<button
onclick={remove}
class="btn rounded-lg h-fit py-2 w-full"
disabled={submitting}
>{m["convert.errors.vertd_generic_no"]()}</button
>
</div>
</div>

View File

@ -1,19 +1,14 @@
<script lang="ts">
import Toast from "$lib/components/visual/Toast.svelte";
import { type Toast as ToastType, toasts } from "$lib/store/ToastProvider";
let toastList = $state<ToastType[]>([]);
toasts.subscribe((value) => {
toastList = value as ToastType[];
});
import { ToastManager } from "$lib/toast/index.svelte";
</script>
<div
class="fixed bottom-28 md:bottom-0 right-0 p-4 flex flex-col-reverse gap-4 z-50"
>
{#each toastList as { id, type, message, durations }}
{#each ToastManager.toasts as toast (toast.id)}
<div class="flex justify-end">
<Toast {id} {type} {message} {durations} />
<Toast {toast} />
</div>
{/each}
</div>

View File

@ -8,20 +8,20 @@
XIcon,
} from "lucide-svelte";
import { quintOut } from "svelte/easing";
import { removeToast } from "$lib/store/ToastProvider";
import { ToastManager } from "$lib/toast/index.svelte";
import type { ToastProps } from "$lib/toast/index.svelte";
import type { SvelteComponent } from "svelte";
import clsx from "clsx";
import type { Toast as ToastType } from "$lib/toast/index.svelte";
type Props = {
id: number;
type: "success" | "error" | "info" | "warning";
message: string;
durations: {
enter: number;
stay: number;
exit: number;
};
};
const props: {
toast: ToastType<unknown>;
} = $props();
let { id, type, message, durations }: Props = $props();
const { id, type, message, durations } = props.toast;
const additional =
"additional" in props.toast ? props.toast.additional : {};
const colors = {
success: "purple",
@ -40,6 +40,9 @@
let color = $derived(colors[type]);
let Icon = $derived(Icons[type]);
let msg = $state<SvelteComponent<ToastProps>>();
const title = $derived(((msg as any)?.title as string) ?? "");
// intentionally unused. this is so tailwind can generate the css for these colours as it doesn't detect if it's dynamically loaded
// this would lead to the colours not being generated in the final css file by tailwind
const colourVariants = [
@ -51,7 +54,7 @@
</script>
<div
class="flex items-center justify-between max-w-[100%] md:max-w-md p-4 gap-4 bg-accent-{color} border-accent-{color}-alt border-l-4 rounded-lg shadow-md"
class="flex flex-col max-w-[100%] md:max-w-md p-4 gap-2 bg-accent-{color} border-accent-{color}-alt border-l-4 rounded-lg shadow-md"
in:fly={{
duration: durations.enter,
easing: quintOut,
@ -63,21 +66,40 @@
easing: quintOut,
}}
>
<div class="flex items-center gap-4">
<Icon
class="w-6 h-6 text-black flex-shrink-0"
size="32"
stroke="2"
fill="none"
/>
<p class="text-black font-normal whitespace-pre-wrap break-all">
{message}
</p>
<div class="flex flex-row items-center justify-between w-full gap-4">
<div class="flex items-center gap-2">
<Icon
class="w-6 h-6 text-black flex-shrink-0"
size="24"
stroke="2"
fill="none"
/>
<p
class={clsx("text-black whitespace-pre-wrap", {
"font-normal": !title,
})}
>
{title || message}
</p>
</div>
<button
class="text-gray-600 hover:text-black flex-shrink-0"
onclick={() => ToastManager.remove(id)}
>
<XIcon size="16" />
</button>
</div>
<button
class="text-gray-600 hover:text-black flex-shrink-0"
onclick={() => removeToast(id)}
>
<XIcon size="16" />
</button>
{#if typeof message !== "string"}
{@const MessageComponent = message}
<div class="font-normal">
<MessageComponent
bind:this={msg}
{durations}
{id}
{message}
{type}
{additional}
/>
</div>
{/if}
</div>

View File

@ -3,9 +3,9 @@ import { Converter, FormatInfo } from "./converter.svelte";
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";
import { Settings } from "$lib/sections/settings/index.svelte";
import { ToastManager } from "$lib/toast/index.svelte";
// TODO: differentiate in UI? (not native formats)
const videoFormats = [
@ -98,7 +98,10 @@ export class FFmpegConverter extends Converter {
} catch (err) {
error(["converters", this.name], `error loading ffmpeg: ${err}`);
this.status = "error";
addToast("error", m["workers.errors.ffmpeg"]());
ToastManager.add({
type: "error",
message: m["workers.errors.ffmpeg"](),
});
}
}

View File

@ -1,13 +1,13 @@
import { browser } from "$app/environment";
import { error, log } from "$lib/logger";
import { m } from "$lib/paraglide/messages";
import { addToast } from "$lib/store/ToastProvider";
import { VertFile, type WorkerMessage } from "$lib/types";
import MagickWorker from "$lib/workers/magick?worker&url";
import { Converter, FormatInfo } from "./converter.svelte";
import { imageFormats } from "./magick-automated";
import { Settings } from "$lib/sections/settings/index.svelte";
import magickWasm from "@imagemagick/magick-wasm/magick.wasm?url";
import { ToastManager } from "$lib/toast/index.svelte";
export class MagickConverter extends Converter {
public name = "imagemagick";
@ -104,7 +104,11 @@ export class MagickConverter extends Converter {
["converters", this.name],
`Failed to load ImageMagick WASM: ${err}`,
);
addToast("error", m["workers.errors.magick"]());
ToastManager.add({
type: "error",
message: m["workers.errors.magick"](),
});
}
}

View File

@ -2,8 +2,9 @@ import { VertFile, type WorkerMessage } from "$lib/types";
import { Converter, FormatInfo } from "./converter.svelte";
import { browser } from "$app/environment";
import PandocWorker from "$lib/workers/pandoc?worker&url";
import { addToast } from "$lib/store/ToastProvider";
import { error, log } from "$lib/logger";
import { ToastManager } from "$lib/toast/index.svelte";
import { m } from "$lib/paraglide/messages";
export class PandocConverter extends Converter {
public name = "pandoc";
@ -25,7 +26,10 @@ export class PandocConverter extends Converter {
this.status = "ready";
} catch (err) {
this.status = "error";
addToast("error", `Failed to load Pandoc worker: ${err}`);
ToastManager.add({
type: "error",
message: `Failed to load Pandoc worker: ${err}`, // TODO: i18n
});
}
})();
}

View File

@ -1,3 +1,4 @@
import VertdErrorComponent from "$lib/components/functional/VertdError.svelte";
import { error, log } from "$lib/logger";
import { Settings } from "$lib/sections/settings/index.svelte";
import { VertdInstance } from "$lib/sections/settings/vertdSettings.svelte";
@ -25,19 +26,45 @@ interface UploadResponse {
totalFrames: number;
}
interface RouteMap {
"/api/upload": UploadResponse;
"/api/version": string;
interface RouteRequestMap {
"/api/keep": {
id: string;
token: string;
};
}
const vertdFetch = async <U extends keyof RouteMap>(
url: U,
options: RequestInit,
): Promise<RouteMap[U]> => {
interface RouteResponseMap {
"/api/upload": UploadResponse;
"/api/version": string;
"/api/keep": void;
}
export const vertdFetch: {
<U extends keyof RouteRequestMap>(
url: U,
options: RequestInit,
body: RouteRequestMap[U],
): Promise<RouteResponseMap[U]>;
<U extends Exclude<keyof RouteResponseMap, keyof RouteRequestMap>>(
url: U,
options: RequestInit,
): Promise<RouteResponseMap[U]>;
} = async (url: any, options: RequestInit, body?: any) => {
const domain = await VertdInstance.instance.url();
const res = await fetch(`${domain}${url}`, options);
// if there is a body, insert a Content-Type: application/json header
if (body) {
options.headers = {
"Content-Type": "application/json",
...(options.headers || {}),
};
options.body = JSON.stringify(body);
}
const res = await fetch(domain + url, options);
const text = await res.text();
let json: VertdResponse<RouteMap[U]> = null!;
let json = null;
try {
json = JSON.parse(text);
} catch {
@ -48,7 +75,7 @@ const vertdFetch = async <U extends keyof RouteMap>(
throw new Error(json.data);
}
return json.data as RouteMap[U];
return json.data;
};
// ws types
@ -345,7 +372,13 @@ export class VertdConverter extends Converter {
case "error": {
this.log(`error: ${msg.data.message}`);
this.activeConversions.delete(input.id);
reject(msg.data.message);
reject({
component: VertdErrorComponent,
additional: {
jobId: uploadRes.id,
auth: uploadRes.auth,
},
});
}
}
};

View File

@ -22,7 +22,6 @@
import FancyInput from "$lib/components/functional/FancyInput.svelte";
import Panel from "$lib/components/visual/Panel.svelte";
import { effects } from "$lib/store/index.svelte";
import { addToast } from "$lib/store/ToastProvider";
import {
loadStripe,
type Stripe,
@ -39,6 +38,7 @@
import { Elements, PaymentElement } from "svelte-stripe";
import { quintOut } from "svelte/easing";
import { m } from "$lib/paraglide/messages";
import { ToastManager } from "$lib/toast/index.svelte";
let amount = $state(1);
let customAmount = $state("");
@ -67,7 +67,10 @@
if (!res.ok) {
paymentState = "prepay";
addToast("error", m["about.donate.payment_error"]());
ToastManager.add({
type: "error",
message: m["about.donate.payment_error"](),
});
return;
}
@ -97,13 +100,13 @@
const submitResult = await elements.submit();
if (submitResult.error) {
const period = submitResult.error.message?.endsWith(".") ? "" : ".";
addToast(
"error",
m["about.donate.payment_failed"]({
ToastManager.add({
type: "error",
message: m["about.donate.payment_failed"]({
message: submitResult.error.message || "",
period,
}),
);
});
enablePay = true;
return;
}
@ -119,15 +122,18 @@
if (res.error) {
const period = res.error.message?.endsWith(".") ? "" : ".";
addToast(
"error",
m["about.donate.payment_failed"]({
ToastManager.add({
type: "error",
message: m["about.donate.payment_failed"]({
message: res.error.message || "",
period,
}),
);
});
} else {
addToast("success", m["about.donate.thank_you"]());
ToastManager.add({
type: "info",
message: m["about.donate.thank_you"](),
});
}
paymentState = "prepay";
@ -146,10 +152,16 @@
if (status) {
switch (status) {
case "succeeded":
addToast("success", m["about.donate.thank_you"]());
ToastManager.add({
type: "success",
message: m["about.donate.thank_you"](),
});
break;
default:
addToast("error", m["about.donate.donation_error"]());
ToastManager.add({
type: "error",
message: m["about.donate.donation_error"](),
});
}
goto("/about");

View File

@ -4,9 +4,9 @@
import HotMilk from "$lib/assets/hotmilk.svg?component";
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";
import { ToastManager } from "$lib/toast/index.svelte";
let copied = false;
let timeoutId: number | undefined;
@ -15,7 +15,10 @@
try {
navigator.clipboard.writeText("hello@vert.sh");
copied = true;
addToast("success", m["about.sponsors.email_copied"]());
ToastManager.add({
type: "success",
message: m["about.sponsors.email_copied"](),
});
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => (copied = false), 2000);

View File

@ -12,9 +12,9 @@
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
import { swManager, type CacheInfo } from "$lib/sw/register";
import { addToast } from "$lib/store/ToastProvider";
import { onMount } from "svelte";
import { error } from "$lib/logger";
import { ToastManager } from "$lib/toast/index.svelte";
const { settings = $bindable() }: { settings: ISettings } = $props();
@ -50,10 +50,16 @@
await swManager.clearCache();
cacheInfo = null;
await loadCacheInfo();
addToast("success", m["settings.privacy.cache_cleared"]());
ToastManager.add({
type: "success",
message: m["settings.privacy.cache_cleared"](),
});
} catch (err) {
error(["privacy", "cache"], "Failed to clear cache:", err);
addToast("error", "Failed to clear cache");
ToastManager.add({
type: "error",
message: m["settings.privacy.cache_clear_error"](),
});
} finally {
isLoadingCache = false;
}

View File

@ -1,60 +0,0 @@
import { writable } from "svelte/store";
export type ToastType = "success" | "error" | "info" | "warning";
export interface Toast {
id: number;
type: ToastType;
message: string;
disappearing: boolean;
durations: {
enter: number;
stay: number;
exit: number;
};
}
const toasts = writable<Toast[]>([]);
let toastId = 0;
function addToast(
type: ToastType,
message: string,
disappearing?: boolean,
durations?: { enter: number; stay: number; exit: number },
) {
const id = toastId++;
durations = durations ?? {
enter: 300,
stay: disappearing || disappearing === undefined ? 5000 : 86400000, // 24h cause why not
exit: 500,
};
const newToast: Toast = {
id,
type,
message,
disappearing: disappearing ?? true,
durations,
};
toasts.update((currentToasts) => [...currentToasts, newToast]);
setTimeout(
() => {
removeToast(id);
},
durations.enter + durations.stay + durations.exit,
);
return id;
}
function removeToast(id: number) {
toasts.update((currentToasts) =>
currentToasts.filter((toast) => toast.id !== id),
);
}
export { toasts, addToast, removeToast };

View File

@ -323,6 +323,8 @@ export const availableLocales = {
fr: "Français",
de: "Deutsch",
hr: "Hrvatski",
tr: "Türkçe",
ja: "日本語",
};
export function updateLocale(newLocale: string) {

View File

@ -0,0 +1,226 @@
import type { Component } from "svelte";
import { writable } from "svelte/store";
export type ToastType = "success" | "error" | "info" | "warning";
// export interface Toast<
// T = unknown,
// U extends string | ToastComponent<T> = string | ToastComponent<T>,
// > {
// id: number;
// type: ToastType;
// message: U;
// disappearing: boolean;
// durations: {
// enter: number;
// stay: number;
// exit: number;
// };
// additional: U extends string ? undefined : T;
// }
type BaseToast = {
id: number;
type: ToastType;
disappearing: boolean;
durations: {
enter: number;
stay: number;
exit: number;
};
};
export type StringToast = BaseToast & {
message: string;
};
export type ComponentToast<T> = BaseToast & {
message: ToastComponent<T>;
additional: T;
};
export type Toast<T = unknown> = StringToast | ComponentToast<T>;
export type ToastProps<T = unknown> = Omit<ComponentToast<T>, "disappearing">;
export type ToastExports = {
title?: string;
};
export type ToastComponent<T> = Component<ToastProps<T>, ToastExports>;
// export interface ToastOptions<T = unknown> {
// type?: ToastType;
// message: string | ToastComponent<T>;
// disappearing?: boolean;
// durations?: {
// enter?: number;
// stay?: number;
// exit?: number;
// };
// additional?: T;
// }
type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
? RecursivePartial<U>[]
: T[P] extends object | undefined
? RecursivePartial<T[P]>
: T[P];
};
type BaseToastOptions = Omit<RecursivePartial<BaseToast>, "id"> & {
disappearing?: boolean;
};
export type StringToastOptions = BaseToastOptions & {
message: string;
};
export type ComponentToastOptions<T> = BaseToastOptions & {
message: ToastComponent<T>;
additional: T;
};
export type ToastOptions<T = unknown> =
| StringToastOptions
| ComponentToastOptions<T>;
// const toasts = writable<Toast[]>([]);
// let toastId = 0;
// function addToast(
// type: ToastType,
// message: string | Component,
// disappearing?: boolean,
// durations?: { enter: number; stay: number; exit: number },
// ) {
// const id = toastId++;
// durations = durations ?? {
// enter: 300,
// stay: disappearing || disappearing === undefined ? 5000 : 86400000, // 24h cause why not
// exit: 500,
// };
// const newToast: Toast = {
// id,
// type,
// message,
// disappearing: disappearing ?? true,
// durations,
// };
// toasts.update((currentToasts) => [...currentToasts, newToast]);
// setTimeout(
// () => {
// removeToast(id);
// },
// durations.enter + durations.stay + durations.exit,
// );
// return id;
// }
// function removeToast(id: number) {
// toasts.update((currentToasts) =>
// currentToasts.filter((toast) => toast.id !== id),
// );
// }
// export { toasts, addToast, removeToast };
// const DURATION_DEFAULTS = {
// enter: 300,
// stay: 5000,
// exit: 500,
// };
const durationDefault = (disappearing: boolean) => ({
enter: 300,
stay: disappearing ? 5000 : 86400000, // 24h cause why not
exit: 500,
});
// const toastState = {
// toasts: $state<Toast[]>([]),
// };
class ToastState {
private pId = $state(0);
private pToasts = $state<Toast<unknown>[]>([]);
public add<T>(toast: Toast<T>) {
this.pToasts.push(toast as Toast<unknown>);
}
public remove(id: number) {
this.pToasts = this.pToasts.filter((toast) => toast.id !== id);
}
public id(): number {
return this.pId++;
}
public get toasts() {
return this.pToasts;
}
}
export class ToastManager {
static pToasts = new ToastState();
public static add<T = unknown>(toastOptions: ToastOptions<T>): number {
const id = this.pToasts.id();
const {
type = "info",
disappearing = true,
durations: d = durationDefault(toastOptions.disappearing ?? true),
} = toastOptions;
const durations = {
...durationDefault(disappearing),
...d,
};
if (typeof toastOptions.message === "string") {
const newToast: StringToast = {
id,
type,
message: toastOptions.message,
disappearing,
durations,
};
this.pToasts.add(newToast);
} else {
const newToast: ComponentToast<T> = {
id,
type,
message: toastOptions.message,
disappearing,
durations,
additional: (toastOptions as ComponentToastOptions<T>)
.additional,
};
this.pToasts.add(newToast);
}
setTimeout(
() => {
this.remove(id);
},
durations.enter + durations.stay + durations.exit,
);
return id;
}
public static remove(id: number) {
this.pToasts.remove(id);
}
public static get toasts() {
return this.pToasts.toasts;
}
}

View File

@ -2,7 +2,8 @@ import { byNative, 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";
import { ToastManager } from "$lib/toast/index.svelte";
import type { Component } from "svelte";
export class VertFile {
public id: string = Math.random().toString(36).slice(2, 8);
@ -93,15 +94,7 @@ export class VertFile {
this.result = res;
} catch (err) {
if (!this.cancelled) {
const castedErr = err as Error;
error(["files"], castedErr.message);
addToast(
"error",
m["workers.errors.general"]({
file: this.file.name,
message: castedErr.message || castedErr,
}),
);
this.toastErr(err);
}
this.result = null;
}
@ -119,15 +112,51 @@ export class VertFile {
this.processing = false;
this.result = null;
} catch (err) {
const castedErr = err as Error;
error(["files"], castedErr.message);
addToast(
"error",
m["workers.errors.cancel"]({
this.toastErr(err);
}
}
private toastErr(err: unknown) {
type ToastMsg = {
component: Component;
additional: unknown;
};
const castedErr = err as Error | string | ToastMsg;
let toastMsg: string | ToastMsg = "";
if (typeof castedErr === "string") {
toastMsg = castedErr;
} else if (castedErr instanceof Error) {
toastMsg = castedErr.message;
} else {
toastMsg = castedErr;
}
// ToastManager.add({
// type: "error",
// message:
// typeof toastMsg === "string"
// ? m["workers.errors.general"]({
// file: this.file.name,
// message: toastMsg,
// })
// : toastMsg,
// });
if (typeof toastMsg === "string") {
ToastManager.add({
type: "error",
message: m["workers.errors.general"]({
file: this.file.name,
message: castedErr.message || castedErr,
message: toastMsg,
}),
);
});
} else {
ToastManager.add({
type: "error",
message: toastMsg.component,
additional: toastMsg.additional,
});
}
}

View File

@ -127,6 +127,7 @@
name="description"
content="With VERT you can quickly convert any image, video and audio file. No ads, no tracking, open source, and all processing (other than video) is done on your device."
/>
<meta property="og:url" content="https://vert.sh">
<meta property="og:type" content="website" />
<meta
property="og:title"
@ -137,7 +138,9 @@
content="With VERT you can quickly convert any image, video and audio file. No ads, no tracking, open source, and all processing (other than video) is done on your device."
/>
<meta property="og:image" content={featuredImage} />
<meta property="twitter:card" content="summary_large_image" />
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="vert.sh">
<meta property="twitter:url" content="https://vert.sh">
<meta
property="twitter:title"
content="{VERT_NAME} — Free, fast, and awesome file converter"

View File

@ -9,10 +9,10 @@
import avatarRealmy from "$lib/assets/avatars/realmy.jpg";
import avatarAzurejelly from "$lib/assets/avatars/azurejelly.jpg";
import { GITHUB_API_URL } from "$lib/consts";
import { addToast } from "$lib/store/ToastProvider";
import { dev } from "$app/environment";
import { page } from "$app/state";
import { m } from "$lib/paraglide/messages";
import { ToastManager } from "$lib/toast/index.svelte";
// import { dev } from "$app/environment";
// import { page } from "$app/state";
@ -81,7 +81,10 @@
try {
const response = await fetch(`${GITHUB_API_URL}/contributors`);
if (!response.ok) {
addToast("error", m["about.errors.github_contributors"]());
ToastManager.add({
type: "error",
message: m["about.errors.github_contributors"](),
});
throw new Error(`HTTP error, status: ${response.status}`);
}
const allContribs = await response.json();

View File

@ -2,11 +2,11 @@
import { browser } from "$app/environment";
import { log } from "$lib/logger";
import * as Settings from "$lib/sections/settings/index.svelte";
import { addToast } from "$lib/store/ToastProvider";
import { PUB_PLAUSIBLE_URL } from "$env/static/public";
import { SettingsIcon } from "lucide-svelte";
import { onMount } from "svelte";
import { m } from "$lib/paraglide/messages";
import { ToastManager } from "$lib/toast/index.svelte";
let settings = $state(Settings.Settings.instance.settings);
@ -32,7 +32,10 @@
log(["settings"], "saving settings");
} catch (error) {
log(["settings", "error"], `failed to save settings: ${error}`);
addToast("error", m["settings.errors.save_failed"]());
ToastManager.add({
type: "error",
message: m["settings.errors.save_failed"](),
});
}
});