diff --git a/ui/messages/en.json b/ui/messages/en.json new file mode 100644 index 0000000..a48d0ae --- /dev/null +++ b/ui/messages/en.json @@ -0,0 +1,384 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + + "nav_library": "Library", + "nav_catalogue": "Catalogue", + "nav_feedback": "Feedback", + "nav_admin": "Admin", + "nav_profile": "Profile", + "nav_sign_in": "Sign in", + "nav_sign_out": "Sign out", + "nav_toggle_menu": "Toggle menu", + "nav_admin_panel": "Admin panel", + + "footer_library": "Library", + "footer_catalogue": "Catalogue", + "footer_feedback": "Feedback", + "footer_disclaimer": "Disclaimer", + "footer_privacy": "Privacy", + "footer_dmca": "DMCA", + "footer_copyright": "© {year} libnovel", + "footer_dev": "dev", + + "home_title": "libnovel", + "home_stat_books": "Books", + "home_stat_chapters": "Chapters", + "home_stat_in_progress": "In progress", + "home_continue_reading": "Continue Reading", + "home_view_all": "View all", + "home_recently_updated": "Recently Updated", + "home_from_following": "From People You Follow", + "home_empty_title": "Your library is empty", + "home_empty_body": "Discover novels and scrape them into your library.", + "home_discover_novels": "Discover Novels", + "home_via_reader": "via {username}", + "home_chapter_badge": "ch.{n}", + + "player_generating": "Generating… {percent}%", + "player_loading": "Loading…", + "player_chapters": "Chapters", + "player_chapter_n": "Chapter {n}", + "player_toggle_chapter_list": "Toggle chapter list", + "player_chapter_list_label": "Chapter list", + "player_close_chapter_list": "Close chapter list", + "player_rewind_15": "Rewind 15 seconds", + "player_skip_30": "Skip 30 seconds", + "player_back_15": "Back 15s", + "player_forward_30": "Forward 30s", + "player_play": "Play", + "player_pause": "Pause", + "player_speed_label": "Playback speed {speed}x", + "player_change_speed": "Change playback speed", + "player_auto_next_on": "Auto-next on", + "player_auto_next_off": "Auto-next off", + "player_auto_next_ready": "Auto-next on — Ch.{n} ready", + "player_auto_next_preparing": "Auto-next on — preparing Ch.{n}…", + "player_auto_next_aria": "Auto-next {state}", + "player_go_to_chapter": "Go to chapter", + "player_close": "Close player", + + "login_page_title": "Sign in — libnovel", + "login_heading": "Sign in to libnovel", + "login_subheading": "Choose a provider to continue", + "login_continue_google": "Continue with Google", + "login_continue_github": "Continue with GitHub", + "login_terms_notice": "By signing in you agree to our terms of service.", + "login_error_oauth_state": "Sign-in was cancelled or expired. Please try again.", + "login_error_oauth_failed": "Could not connect to the provider. Please try again.", + "login_error_oauth_no_email": "Your account has no verified email address. Please add one and retry.", + + "books_page_title": "Library — libnovel", + "books_heading": "Your Library", + "books_empty_title": "No books yet", + "books_empty_body": "Add books to your library by visiting a book page.", + "books_browse_catalogue": "Browse Catalogue", + "books_chapter_count": "{n} chapters", + "books_last_read": "Last read: Ch.{n}", + "books_reading_progress": "Ch.{current} / {total}", + "books_remove": "Remove", + + "catalogue_page_title": "Catalogue — libnovel", + "catalogue_heading": "Catalogue", + "catalogue_search_placeholder": "Search novels…", + "catalogue_filter_genre": "Genre", + "catalogue_filter_status": "Status", + "catalogue_filter_sort": "Sort", + "catalogue_sort_popular": "Popular", + "catalogue_sort_new": "New", + "catalogue_sort_top_rated": "Top Rated", + "catalogue_sort_rank": "Rank", + "catalogue_status_all": "All", + "catalogue_status_ongoing": "Ongoing", + "catalogue_status_completed": "Completed", + "catalogue_genre_all": "All genres", + "catalogue_clear_filters": "Clear", + "catalogue_reset": "Reset", + "catalogue_no_results": "No novels found.", + "catalogue_loading": "Loading…", + "catalogue_load_more": "Load more", + "catalogue_results_count": "{n} results", + + "book_detail_page_title": "{title} — libnovel", + "book_detail_add_to_library": "Add to Library", + "book_detail_remove_from_library": "Remove from Library", + "book_detail_read_now": "Read Now", + "book_detail_continue_reading": "Continue Reading", + "book_detail_start_reading": "Start Reading", + "book_detail_chapters": "{n} Chapters", + "book_detail_status": "Status", + "book_detail_author": "Author", + "book_detail_genres": "Genres", + "book_detail_description": "Description", + "book_detail_source": "Source", + "book_detail_rescrape": "Re-scrape", + "book_detail_scraping": "Scraping…", + "book_detail_in_library": "In Library", + + "chapters_page_title": "Chapters — {title}", + "chapters_heading": "Chapters", + "chapters_back_to_book": "Back to book", + "chapters_reading_now": "Reading", + "chapters_empty": "No chapters scraped yet.", + + "reader_page_title": "{title} — Ch.{n} — libnovel", + "reader_play_narration": "Play narration", + "reader_generating_audio": "Generating audio…", + "reader_audio_error": "Audio generation failed.", + "reader_prev_chapter": "Previous chapter", + "reader_next_chapter": "Next chapter", + "reader_back_to_chapters": "Back to chapters", + "reader_chapter_n": "Chapter {n}", + "reader_change_voice": "Change voice", + "reader_voice_panel_title": "Select voice", + "reader_voice_kokoro": "Kokoro voices", + "reader_voice_pocket": "Pocket-TTS voices", + "reader_voice_play_sample": "Play sample", + "reader_voice_stop_sample": "Stop sample", + "reader_voice_selected": "Selected", + "reader_close_voice_panel": "Close voice panel", + "reader_auto_next": "Auto-next", + "reader_speed": "Speed", + "reader_preview_notice": "Preview — this chapter has not been fully scraped.", + + "profile_page_title": "Profile — libnovel", + "profile_heading": "Profile", + "profile_avatar_label": "Avatar", + "profile_change_avatar": "Change avatar", + "profile_username": "Username", + "profile_email": "Email", + "profile_change_password": "Change password", + "profile_current_password": "Current password", + "profile_new_password": "New password", + "profile_confirm_password": "Confirm password", + "profile_save_password": "Save password", + "profile_appearance_heading": "Appearance", + "profile_theme_label": "Theme", + "profile_theme_amber": "Amber", + "profile_theme_slate": "Slate", + "profile_theme_rose": "Rose", + "profile_reading_heading": "Reading settings", + "profile_voice_label": "Default voice", + "profile_speed_label": "Playback speed", + "profile_auto_next_label": "Auto-next chapter", + "profile_save_settings": "Save settings", + "profile_settings_saved": "Settings saved.", + "profile_settings_error": "Failed to save settings.", + "profile_password_saved": "Password changed.", + "profile_password_error": "Failed to change password.", + "profile_sessions_heading": "Active sessions", + "profile_sign_out_all": "Sign out all other devices", + "profile_joined": "Joined {date}", + + "user_page_title": "{username} — libnovel", + "user_library_heading": "{username}'s Library", + "user_follow": "Follow", + "user_unfollow": "Unfollow", + "user_followers": "{n} followers", + "user_following": "{n} following", + "user_library_empty": "No books in library.", + + "error_not_found_title": "Page not found", + "error_not_found_body": "The page you're looking for doesn't exist.", + "error_generic_title": "Something went wrong", + "error_go_home": "Go home", + "error_status": "Error {status}", + + "admin_scrape_page_title": "Scrape — Admin", + "admin_scrape_heading": "Scrape", + "admin_scrape_catalogue": "Scrape Catalogue", + "admin_scrape_book": "Scrape Book", + "admin_scrape_url_placeholder": "novelfire.net book URL", + "admin_scrape_range": "Chapter range", + "admin_scrape_from": "From", + "admin_scrape_to": "To", + "admin_scrape_submit": "Scrape", + "admin_scrape_cancel": "Cancel", + "admin_scrape_status_pending": "Pending", + "admin_scrape_status_running": "Running", + "admin_scrape_status_done": "Done", + "admin_scrape_status_failed": "Failed", + "admin_scrape_status_cancelled": "Cancelled", + "admin_tasks_heading": "Recent tasks", + "admin_tasks_empty": "No tasks yet.", + + "admin_audio_page_title": "Audio — Admin", + "admin_audio_heading": "Audio Jobs", + "admin_audio_empty": "No audio jobs.", + + "admin_changelog_page_title": "Changelog — Admin", + "admin_changelog_heading": "Changelog", + + "comments_heading": "Comments", + "comments_empty": "No comments yet. Be the first!", + "comments_placeholder": "Write a comment…", + "comments_submit": "Post", + "comments_login_prompt": "Sign in to comment.", + "comments_vote_up": "Upvote", + "comments_vote_down": "Downvote", + "comments_delete": "Delete", + "comments_reply": "Reply", + "comments_show_replies": "Show {n} replies", + "comments_hide_replies": "Hide replies", + "comments_edited": "edited", + "comments_deleted": "[deleted]", + + "disclaimer_page_title": "Disclaimer — libnovel", + "privacy_page_title": "Privacy Policy — libnovel", + "dmca_page_title": "DMCA — libnovel", + "terms_page_title": "Terms of Service — libnovel", + + "common_loading": "Loading…", + "common_error": "Error", + "common_save": "Save", + "common_cancel": "Cancel", + "common_close": "Close", + "common_search": "Search", + "common_back": "Back", + "common_next": "Next", + "common_previous": "Previous", + "common_yes": "Yes", + "common_no": "No", + "common_on": "on", + "common_off": "off", + + "locale_switcher_label": "Language", + + "books_empty_library": "Your library is empty.", + "books_empty_discover": "Books you start reading or save from", + "books_empty_discover_link": "Discover", + "books_empty_discover_suffix": "will appear here.", + "books_count": "{n} book{s}", + + "catalogue_sort_updated": "Updated", + "catalogue_search_button": "Search", + "catalogue_refresh": "Refresh", + "catalogue_refreshing": "Queuing\u2026", + "catalogue_refresh_mobile": "Refresh catalogue", + "catalogue_all_loaded": "All novels loaded", + "catalogue_scroll_top": "Back to top", + "catalogue_view_grid": "Grid view", + "catalogue_view_list": "List view", + "catalogue_browse_source": "Browse novels from novelfire.net", + "catalogue_search_results": "{n} result{s} for \"{q}\"", + "catalogue_search_local_count": "({local} local, {remote} from novelfire)", + "catalogue_rank_ranked": "{n} novels ranked from last catalogue scrape", + "catalogue_rank_no_data": "No ranking data.", + "catalogue_rank_no_data_body": "No ranking data \u2014 run a full catalogue scrape to populate", + "catalogue_rank_run_scrape_admin": "Click Refresh catalogue above to trigger a full catalogue scrape.", + "catalogue_rank_run_scrape_user": "Ask an admin to run a catalogue scrape.", + "catalogue_scrape_queued_flash": "Full catalogue scrape queued. Library and ranking will update as books are processed.", + "catalogue_scrape_busy_flash": "A scrape job is already running. Check back once it finishes.", + "catalogue_scrape_error_flash": "Failed to queue scrape. Check that the scraper service is reachable.", + "catalogue_filters_label": "Filters", + "catalogue_apply": "Apply", + "catalogue_filter_rank_note": "Genre & status filters apply to Browse only", + "catalogue_no_results_search": "No results found.", + "catalogue_no_results_try": "Try a different search term.", + "catalogue_no_results_filters": "Try different filters or check back later.", + "catalogue_scrape_queued_badge": "Queued", + "catalogue_scrape_busy_badge": "Scraper busy", + "catalogue_scrape_busy_list": "Busy", + "catalogue_scrape_forbidden_badge": "Forbidden", + "catalogue_scrape_novel_button": "Scrape", + "catalogue_scraping_novel": "Scraping\u2026", + + "book_detail_not_in_library": "not in library", + "book_detail_continue_ch": "Continue ch.{n}", + "book_detail_start_ch1": "Start from ch.1", + "book_detail_preview_ch1": "Preview ch.1", + "book_detail_reading_ch": "Reading ch.{n} of {total}", + "book_detail_n_chapters": "{n} chapters", + "book_detail_rescraping": "Queuing\u2026", + "book_detail_from_chapter": "From chapter", + "book_detail_to_chapter": "To chapter (optional)", + "book_detail_range_queuing": "Queuing\u2026", + "book_detail_scrape_range": "Scrape range", + "book_detail_admin": "Admin", + "book_detail_scraping_progress": "Fetching the first 20 chapters. This page will refresh automatically.", + "book_detail_scraping_home": "\u2190 Home", + "book_detail_rescrape_book": "Rescrape book", + "book_detail_less": "Less", + "book_detail_more": "More", + + "chapters_search_placeholder": "Search chapters\u2026", + "chapters_jump_to": "Jump to Ch.{n}", + "chapters_no_match": "No chapters match \"{q}\"", + "chapters_none_available": "No chapters available yet.", + "chapters_reading_indicator": "reading", + "chapters_result_count": "{n} results", + + "reader_fetching_chapter": "Fetching chapter\u2026", + "reader_words": "{n} words", + "reader_preview_audio_notice": "Preview chapter \u2014 audio not available for books outside the library.", + + "profile_click_to_change": "Click avatar to change photo", + "profile_tts_voice": "TTS voice", + "profile_auto_advance": "Auto-advance to next chapter", + "profile_saving": "Saving\u2026", + "profile_saved": "Saved!", + "profile_session_this": "This session", + "profile_session_signed_in": "Signed in {date}", + "profile_session_last_seen": "\u00b7 Last seen {date}", + "profile_session_sign_out": "Sign out", + "profile_session_end": "End", + "profile_session_unrecognised": "These are all devices currently signed into your account. End any session you don\u2019t recognise.", + "profile_no_sessions": "No session records found. Sessions are tracked from the next login.", + "profile_change_password_heading": "Change password", + "profile_update_password": "Update password", + "profile_updating": "Updating\u2026", + "profile_password_changed_ok": "Password changed successfully.", + "profile_playback_speed": "Playback speed \u2014 {speed}x", + + "user_currently_reading": "Currently Reading", + "user_library_count": "Library ({n})", + "user_joined": "Joined {date}", + "user_followers_label": "followers", + "user_following_label": "following", + "user_no_books": "No books in library yet.", + + "admin_pages_label": "Pages", + "admin_tools_label": "Tools", + + "admin_scrape_status_idle": "Idle", + "admin_scrape_status_running": "Running", + "admin_scrape_full_catalogue": "Full catalogue", + "admin_scrape_single_book": "Single book", + "admin_scrape_quick_genres": "Quick genres", + "admin_scrape_task_history": "Task history", + "admin_scrape_filter_placeholder": "Filter by kind, status or URL\u2026", + "admin_scrape_no_matching": "No matching tasks.", + "admin_scrape_start": "Start scrape", + "admin_scrape_queuing": "Queuing\u2026", + "admin_scrape_running": "Running\u2026", + + "admin_audio_filter_jobs": "Filter by slug, voice or status\u2026", + "admin_audio_filter_cache": "Filter by slug, chapter or voice\u2026", + "admin_audio_no_matching_jobs": "No matching jobs.", + "admin_audio_no_jobs": "No audio jobs yet.", + "admin_audio_cache_empty": "Audio cache is empty.", + "admin_audio_no_cache_results": "No results.", + + "admin_changelog_gitea": "Gitea releases", + "admin_changelog_no_releases": "No releases found.", + "admin_changelog_load_error": "Could not load releases: {error}", + + "comments_top": "Top", + "comments_new": "New", + "comments_posting": "Posting\u2026", + "comments_login_link": "Log in", + "comments_login_suffix": "to leave a comment.", + "comments_anonymous": "Anonymous", + + "reader_audio_narration": "Audio Narration", + "reader_playing": "Playing \u2014 controls below", + "reader_paused": "Paused \u2014 controls below", + "reader_ch_ready": "Ch.{n} ready", + "reader_ch_preparing": "Preparing Ch.{n}\u2026 {percent}%", + "reader_ch_generate_on_nav": "Ch.{n} will generate on navigate", + "reader_now_playing": "Now playing: {title}", + "reader_load_this_chapter": "Load this chapter", + "reader_generate_samples": "Generate missing samples", + "reader_voice_applies_next": "New voice applies on next \u201cPlay narration\u201d.", + "reader_choose_voice": "Choose Voice", + "reader_generating_narration": "Generating narration\u2026" +} diff --git a/ui/messages/fr.json b/ui/messages/fr.json new file mode 100644 index 0000000..5958aa8 --- /dev/null +++ b/ui/messages/fr.json @@ -0,0 +1,383 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + + "nav_library": "Bibliothèque", + "nav_catalogue": "Catalogue", + "nav_feedback": "Retour", + "nav_admin": "Admin", + "nav_profile": "Profil", + "nav_sign_in": "Connexion", + "nav_sign_out": "Déconnexion", + "nav_toggle_menu": "Menu", + "nav_admin_panel": "Panneau admin", + + "footer_library": "Bibliothèque", + "footer_catalogue": "Catalogue", + "footer_feedback": "Retour", + "footer_disclaimer": "Avertissement", + "footer_privacy": "Confidentialité", + "footer_dmca": "DMCA", + "footer_copyright": "© {year} libnovel", + "footer_dev": "dev", + + "home_title": "libnovel", + "home_stat_books": "Livres", + "home_stat_chapters": "Chapitres", + "home_stat_in_progress": "En cours", + "home_continue_reading": "Continuer la lecture", + "home_view_all": "Voir tout", + "home_recently_updated": "Récemment mis à jour", + "home_from_following": "Des personnes que vous suivez", + "home_empty_title": "Votre bibliothèque est vide", + "home_empty_body": "Découvrez des romans et ajoutez-les à votre bibliothèque.", + "home_discover_novels": "Découvrir des romans", + "home_via_reader": "via {username}", + "home_chapter_badge": "ch.{n}", + + "player_generating": "Génération… {percent}%", + "player_loading": "Chargement…", + "player_chapters": "Chapitres", + "player_chapter_n": "Chapitre {n}", + "player_toggle_chapter_list": "Liste des chapitres", + "player_chapter_list_label": "Liste des chapitres", + "player_close_chapter_list": "Fermer la liste des chapitres", + "player_rewind_15": "Reculer de 15 secondes", + "player_skip_30": "Avancer de 30 secondes", + "player_back_15": "−15 s", + "player_forward_30": "+30 s", + "player_play": "Lecture", + "player_pause": "Pause", + "player_speed_label": "Vitesse {speed}x", + "player_change_speed": "Changer la vitesse", + "player_auto_next_on": "Suivant auto activé", + "player_auto_next_off": "Suivant auto désactivé", + "player_auto_next_ready": "Suivant auto — Ch.{n} prêt", + "player_auto_next_preparing": "Suivant auto — préparation Ch.{n}…", + "player_auto_next_aria": "Suivant auto {state}", + "player_go_to_chapter": "Aller au chapitre", + "player_close": "Fermer le lecteur", + + "login_page_title": "Connexion — libnovel", + "login_heading": "Se connecter à libnovel", + "login_subheading": "Choisissez un fournisseur pour continuer", + "login_continue_google": "Continuer avec Google", + "login_continue_github": "Continuer avec GitHub", + "login_terms_notice": "En vous connectant, vous acceptez nos conditions d'utilisation.", + "login_error_oauth_state": "Connexion annulée ou expirée. Veuillez réessayer.", + "login_error_oauth_failed": "Impossible de se connecter au fournisseur. Veuillez réessayer.", + "login_error_oauth_no_email": "Votre compte n'a pas d'adresse e-mail vérifiée. Ajoutez-en une et réessayez.", + + "books_page_title": "Bibliothèque — libnovel", + "books_heading": "Votre bibliothèque", + "books_empty_title": "Aucun livre pour l'instant", + "books_empty_body": "Ajoutez des livres à votre bibliothèque en visitant une page de livre.", + "books_browse_catalogue": "Parcourir le catalogue", + "books_chapter_count": "{n} chapitres", + "books_last_read": "Dernier lu : Ch.{n}", + "books_reading_progress": "Ch.{current} / {total}", + "books_remove": "Supprimer", + + "catalogue_page_title": "Catalogue — libnovel", + "catalogue_heading": "Catalogue", + "catalogue_search_placeholder": "Rechercher des romans…", + "catalogue_filter_genre": "Genre", + "catalogue_filter_status": "Statut", + "catalogue_filter_sort": "Trier", + "catalogue_sort_popular": "Populaire", + "catalogue_sort_new": "Nouveau", + "catalogue_sort_top_rated": "Mieux notés", + "catalogue_sort_rank": "Rang", + "catalogue_status_all": "Tous", + "catalogue_status_ongoing": "En cours", + "catalogue_status_completed": "Terminé", + "catalogue_genre_all": "Tous les genres", + "catalogue_clear_filters": "Effacer", + "catalogue_reset": "Réinitialiser", + "catalogue_no_results": "Aucun roman trouvé.", + "catalogue_loading": "Chargement…", + "catalogue_load_more": "Charger plus", + "catalogue_results_count": "{n} résultats", + + "book_detail_page_title": "{title} — libnovel", + "book_detail_add_to_library": "Ajouter à la bibliothèque", + "book_detail_remove_from_library": "Retirer de la bibliothèque", + "book_detail_read_now": "Lire maintenant", + "book_detail_continue_reading": "Continuer la lecture", + "book_detail_start_reading": "Commencer la lecture", + "book_detail_chapters": "{n} chapitres", + "book_detail_status": "Statut", + "book_detail_author": "Auteur", + "book_detail_genres": "Genres", + "book_detail_description": "Description", + "book_detail_source": "Source", + "book_detail_rescrape": "Réextraire", + "book_detail_scraping": "Extraction en cours…", + "book_detail_in_library": "Dans la bibliothèque", + + "chapters_page_title": "Chapitres — {title}", + "chapters_heading": "Chapitres", + "chapters_back_to_book": "Retour au livre", + "chapters_reading_now": "En cours de lecture", + "chapters_empty": "Aucun chapitre extrait pour l'instant.", + + "reader_page_title": "{title} — Ch.{n} — libnovel", + "reader_play_narration": "Lire la narration", + "reader_generating_audio": "Génération audio…", + "reader_audio_error": "Échec de la génération audio.", + "reader_prev_chapter": "Chapitre précédent", + "reader_next_chapter": "Chapitre suivant", + "reader_back_to_chapters": "Retour aux chapitres", + "reader_chapter_n": "Chapitre {n}", + "reader_change_voice": "Changer de voix", + "reader_voice_panel_title": "Sélectionner une voix", + "reader_voice_kokoro": "Voix Kokoro", + "reader_voice_pocket": "Voix Pocket-TTS", + "reader_voice_play_sample": "Écouter un extrait", + "reader_voice_stop_sample": "Arrêter l'extrait", + "reader_voice_selected": "Sélectionné", + "reader_close_voice_panel": "Fermer le panneau vocal", + "reader_auto_next": "Suivant auto", + "reader_speed": "Vitesse", + "reader_preview_notice": "Aperçu — ce chapitre n'a pas été entièrement extrait.", + + "profile_page_title": "Profil — libnovel", + "profile_heading": "Profil", + "profile_avatar_label": "Avatar", + "profile_change_avatar": "Changer l'avatar", + "profile_username": "Nom d'utilisateur", + "profile_email": "E-mail", + "profile_change_password": "Changer le mot de passe", + "profile_current_password": "Mot de passe actuel", + "profile_new_password": "Nouveau mot de passe", + "profile_confirm_password": "Confirmer le mot de passe", + "profile_save_password": "Enregistrer le mot de passe", + "profile_appearance_heading": "Apparence", + "profile_theme_label": "Thème", + "profile_theme_amber": "Ambre", + "profile_theme_slate": "Ardoise", + "profile_theme_rose": "Rose", + "profile_reading_heading": "Paramètres de lecture", + "profile_voice_label": "Voix par défaut", + "profile_speed_label": "Vitesse de lecture", + "profile_auto_next_label": "Chapitre suivant automatique", + "profile_save_settings": "Enregistrer les paramètres", + "profile_settings_saved": "Paramètres enregistrés.", + "profile_settings_error": "Impossible d'enregistrer les paramètres.", + "profile_password_saved": "Mot de passe modifié.", + "profile_password_error": "Impossible de modifier le mot de passe.", + "profile_sessions_heading": "Sessions actives", + "profile_sign_out_all": "Se déconnecter de tous les autres appareils", + "profile_joined": "Inscrit le {date}", + + "user_page_title": "{username} — libnovel", + "user_library_heading": "Bibliothèque de {username}", + "user_follow": "Suivre", + "user_unfollow": "Ne plus suivre", + "user_followers": "{n} abonnés", + "user_following": "{n} abonnements", + "user_library_empty": "Aucun livre dans la bibliothèque.", + + "error_not_found_title": "Page introuvable", + "error_not_found_body": "La page que vous cherchez n'existe pas.", + "error_generic_title": "Une erreur s'est produite", + "error_go_home": "Accueil", + "error_status": "Erreur {status}", + + "admin_scrape_page_title": "Extraction — Admin", + "admin_scrape_heading": "Extraction", + "admin_scrape_catalogue": "Extraire le catalogue", + "admin_scrape_book": "Extraire un livre", + "admin_scrape_url_placeholder": "URL du livre sur novelfire.net", + "admin_scrape_range": "Plage de chapitres", + "admin_scrape_from": "De", + "admin_scrape_to": "À", + "admin_scrape_submit": "Extraire", + "admin_scrape_cancel": "Annuler", + "admin_scrape_status_pending": "En attente", + "admin_scrape_status_running": "En cours", + "admin_scrape_status_done": "Terminé", + "admin_scrape_status_failed": "Échoué", + "admin_scrape_status_cancelled": "Annulé", + "admin_tasks_heading": "Tâches récentes", + "admin_tasks_empty": "Aucune tâche pour l'instant.", + + "admin_audio_page_title": "Audio — Admin", + "admin_audio_heading": "Tâches audio", + "admin_audio_empty": "Aucune tâche audio.", + + "admin_changelog_page_title": "Changelog — Admin", + "admin_changelog_heading": "Changelog", + + "comments_heading": "Commentaires", + "comments_empty": "Aucun commentaire pour l'instant. Soyez le premier !", + "comments_placeholder": "Écrire un commentaire…", + "comments_submit": "Publier", + "comments_login_prompt": "Connectez-vous pour commenter.", + "comments_vote_up": "Vote positif", + "comments_vote_down": "Vote négatif", + "comments_delete": "Supprimer", + "comments_reply": "Répondre", + "comments_show_replies": "Afficher {n} réponses", + "comments_hide_replies": "Masquer les réponses", + "comments_edited": "modifié", + "comments_deleted": "[supprimé]", + + "disclaimer_page_title": "Avertissement — libnovel", + "privacy_page_title": "Politique de confidentialité — libnovel", + "dmca_page_title": "DMCA — libnovel", + "terms_page_title": "Conditions d'utilisation — libnovel", + + "common_loading": "Chargement…", + "common_error": "Erreur", + "common_save": "Enregistrer", + "common_cancel": "Annuler", + "common_close": "Fermer", + "common_search": "Rechercher", + "common_back": "Retour", + "common_next": "Suivant", + "common_previous": "Précédent", + "common_yes": "Oui", + "common_no": "Non", + "common_on": "activé", + "common_off": "désactivé", + + "locale_switcher_label": "Langue", + + "books_empty_library": "Votre bibliothèque est vide.", + "books_empty_discover": "Les livres que vous commencez à lire ou enregistrez depuis", + "books_empty_discover_link": "Découvrir", + "books_empty_discover_suffix": "apparaîtront ici.", + "books_count": "{n} livre{s}", + + "catalogue_sort_updated": "Mis à jour", + "catalogue_search_button": "Rechercher", + "catalogue_refresh": "Actualiser", + "catalogue_refreshing": "En file d'attente…", + "catalogue_refresh_mobile": "Actualiser le catalogue", + "catalogue_all_loaded": "Tous les romans chargés", + "catalogue_scroll_top": "Retour en haut", + "catalogue_view_grid": "Vue grille", + "catalogue_view_list": "Vue liste", + "catalogue_browse_source": "Parcourir les romans de novelfire.net", + "catalogue_search_results": "{n} résultat{s} pour « {q} »", + "catalogue_search_local_count": "({local} local, {remote} depuis novelfire)", + "catalogue_rank_ranked": "{n} romans classés depuis le dernier scrape du catalogue", + "catalogue_rank_no_data": "Aucune donnée de classement.", + "catalogue_rank_no_data_body": "Aucune donnée de classement — lancez un scrape complet du catalogue pour remplir", + "catalogue_rank_run_scrape_admin": "Cliquez sur Actualiser le catalogue ci-dessus pour déclencher un scrape complet.", + "catalogue_rank_run_scrape_user": "Demandez à un administrateur d'effectuer un scrape du catalogue.", + "catalogue_scrape_queued_flash": "Scrape complet du catalogue en file d'attente. La bibliothèque et le classement seront mis à jour au fur et à mesure du traitement des livres.", + "catalogue_scrape_busy_flash": "Un job de scrape est déjà en cours. Revenez une fois terminé.", + "catalogue_scrape_error_flash": "Échec de la mise en file d'attente du scrape. Vérifiez que le service de scraper est accessible.", + "catalogue_filters_label": "Filtres", + "catalogue_apply": "Appliquer", + "catalogue_filter_rank_note": "Les filtres genre et statut s'appliquent uniquement à Parcourir", + "catalogue_no_results_search": "Aucun résultat trouvé.", + "catalogue_no_results_try": "Essayez un autre terme de recherche.", + "catalogue_no_results_filters": "Essayez d'autres filtres ou revenez plus tard.", + "catalogue_scrape_queued_badge": "En file", + "catalogue_scrape_busy_badge": "Scraper occupé", + "catalogue_scrape_busy_list": "Occupé", + "catalogue_scrape_forbidden_badge": "Interdit", + "catalogue_scrape_novel_button": "Extraire", + "catalogue_scraping_novel": "Extraction…", + + "book_detail_not_in_library": "pas dans la bibliothèque", + "book_detail_continue_ch": "Continuer ch.{n}", + "book_detail_start_ch1": "Commencer au ch.1", + "book_detail_preview_ch1": "Aperçu ch.1", + "book_detail_reading_ch": "Lecture ch.{n} sur {total}", + "book_detail_n_chapters": "{n} chapitres", + "book_detail_rescraping": "En file d'attente…", + "book_detail_from_chapter": "À partir du chapitre", + "book_detail_to_chapter": "Jusqu'au chapitre (optionnel)", + "book_detail_range_queuing": "En file d'attente…", + "book_detail_scrape_range": "Plage d'extraction", + "book_detail_admin": "Admin", + "book_detail_scraping_progress": "Récupération des 20 premiers chapitres. Cette page sera actualisée automatiquement.", + "book_detail_scraping_home": "← Accueil", + "book_detail_rescrape_book": "Réextraire le livre", + "book_detail_less": "Moins", + "book_detail_more": "Plus", + + "chapters_search_placeholder": "Rechercher des chapitres…", + "chapters_jump_to": "Aller au Ch.{n}", + "chapters_no_match": "Aucun chapitre ne correspond à « {q} »", + "chapters_none_available": "Aucun chapitre disponible pour l'instant.", + "chapters_reading_indicator": "en cours", + "chapters_result_count": "{n} résultats", + + "reader_fetching_chapter": "Récupération du chapitre…", + "reader_words": "{n} mots", + "reader_preview_audio_notice": "Aperçu — audio non disponible pour les livres hors bibliothèque.", + + "profile_click_to_change": "Cliquez sur l'avatar pour changer la photo", + "profile_tts_voice": "Voix TTS", + "profile_auto_advance": "Avancer automatiquement au chapitre suivant", + "profile_saving": "Enregistrement…", + "profile_saved": "Enregistré !", + "profile_session_this": "Cette session", + "profile_session_signed_in": "Connecté le {date}", + "profile_session_last_seen": "· Dernière activité {date}", + "profile_session_sign_out": "Se déconnecter", + "profile_session_end": "Terminer", + "profile_session_unrecognised": "Ce sont tous les appareils connectés à votre compte. Terminez toute session que vous ne reconnaissez pas.", + "profile_no_sessions": "Aucun enregistrement de session trouvé. Les sessions sont suivies dès la prochaine connexion.", + "profile_change_password_heading": "Changer le mot de passe", + "profile_update_password": "Mettre à jour le mot de passe", + "profile_updating": "Mise à jour…", + "profile_password_changed_ok": "Mot de passe modifié avec succès.", + "profile_playback_speed": "Vitesse de lecture — {speed}x", + + "user_currently_reading": "En cours de lecture", + "user_library_count": "Bibliothèque ({n})", + "user_joined": "Inscrit le {date}", + "user_followers_label": "abonnés", + "user_following_label": "abonnements", + "user_no_books": "Aucun livre dans la bibliothèque pour l'instant.", + + "admin_pages_label": "Pages", + "admin_tools_label": "Outils", + + "admin_scrape_status_idle": "Inactif", + "admin_scrape_full_catalogue": "Catalogue complet", + "admin_scrape_single_book": "Livre unique", + "admin_scrape_quick_genres": "Genres rapides", + "admin_scrape_task_history": "Historique des tâches", + "admin_scrape_filter_placeholder": "Filtrer par type, statut ou URL…", + "admin_scrape_no_matching": "Aucune tâche correspondante.", + "admin_scrape_start": "Démarrer l'extraction", + "admin_scrape_queuing": "En file d'attente…", + "admin_scrape_running": "En cours…", + + "admin_audio_filter_jobs": "Filtrer par slug, voix ou statut…", + "admin_audio_filter_cache": "Filtrer par slug, chapitre ou voix…", + "admin_audio_no_matching_jobs": "Aucun job correspondant.", + "admin_audio_no_jobs": "Aucun job audio pour l'instant.", + "admin_audio_cache_empty": "Cache audio vide.", + "admin_audio_no_cache_results": "Aucun résultat.", + + "admin_changelog_gitea": "Releases Gitea", + "admin_changelog_no_releases": "Aucune release trouvée.", + "admin_changelog_load_error": "Impossible de charger les releases : {error}", + + "comments_top": "Les meilleures", + "comments_new": "Nouvelles", + "comments_posting": "Publication…", + "comments_login_link": "Connectez-vous", + "comments_login_suffix": "pour laisser un commentaire.", + "comments_anonymous": "Anonyme", + + "reader_audio_narration": "Narration Audio", + "reader_playing": "Lecture en cours — contrôles ci-dessous", + "reader_paused": "En pause — contrôles ci-dessous", + "reader_ch_ready": "Ch.{n} prêt", + "reader_ch_preparing": "Préparation Ch.{n}… {percent}%", + "reader_ch_generate_on_nav": "Ch.{n} sera généré lors de la navigation", + "reader_now_playing": "En cours : {title}", + "reader_load_this_chapter": "Charger ce chapitre", + "reader_generate_samples": "Générer les échantillons manquants", + "reader_voice_applies_next": "La nouvelle voix s'appliquera au prochain « Lire la narration ».", + "reader_choose_voice": "Choisir une voix", + "reader_generating_narration": "Génération de la narration…" +} diff --git a/ui/messages/id.json b/ui/messages/id.json new file mode 100644 index 0000000..db15e0c --- /dev/null +++ b/ui/messages/id.json @@ -0,0 +1,383 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + + "nav_library": "Perpustakaan", + "nav_catalogue": "Katalog", + "nav_feedback": "Masukan", + "nav_admin": "Admin", + "nav_profile": "Profil", + "nav_sign_in": "Masuk", + "nav_sign_out": "Keluar", + "nav_toggle_menu": "Menu", + "nav_admin_panel": "Panel admin", + + "footer_library": "Perpustakaan", + "footer_catalogue": "Katalog", + "footer_feedback": "Masukan", + "footer_disclaimer": "Penyangkalan", + "footer_privacy": "Privasi", + "footer_dmca": "DMCA", + "footer_copyright": "© {year} libnovel", + "footer_dev": "dev", + + "home_title": "libnovel", + "home_stat_books": "Buku", + "home_stat_chapters": "Bab", + "home_stat_in_progress": "Sedang dibaca", + "home_continue_reading": "Lanjutkan Membaca", + "home_view_all": "Lihat semua", + "home_recently_updated": "Baru Diperbarui", + "home_from_following": "Dari Orang yang Kamu Ikuti", + "home_empty_title": "Perpustakaanmu kosong", + "home_empty_body": "Temukan novel dan tambahkan ke perpustakaanmu.", + "home_discover_novels": "Temukan Novel", + "home_via_reader": "via {username}", + "home_chapter_badge": "bab.{n}", + + "player_generating": "Membuat… {percent}%", + "player_loading": "Memuat…", + "player_chapters": "Bab", + "player_chapter_n": "Bab {n}", + "player_toggle_chapter_list": "Daftar bab", + "player_chapter_list_label": "Daftar bab", + "player_close_chapter_list": "Tutup daftar bab", + "player_rewind_15": "Mundur 15 detik", + "player_skip_30": "Maju 30 detik", + "player_back_15": "−15 dtk", + "player_forward_30": "+30 dtk", + "player_play": "Putar", + "player_pause": "Jeda", + "player_speed_label": "Kecepatan {speed}x", + "player_change_speed": "Ubah kecepatan", + "player_auto_next_on": "Auto-lanjut aktif", + "player_auto_next_off": "Auto-lanjut nonaktif", + "player_auto_next_ready": "Auto-lanjut — Bab.{n} siap", + "player_auto_next_preparing": "Auto-lanjut — menyiapkan Bab.{n}…", + "player_auto_next_aria": "Auto-lanjut {state}", + "player_go_to_chapter": "Pergi ke bab", + "player_close": "Tutup pemutar", + + "login_page_title": "Masuk — libnovel", + "login_heading": "Masuk ke libnovel", + "login_subheading": "Pilih penyedia untuk melanjutkan", + "login_continue_google": "Lanjutkan dengan Google", + "login_continue_github": "Lanjutkan dengan GitHub", + "login_terms_notice": "Dengan masuk, kamu menyetujui syarat layanan kami.", + "login_error_oauth_state": "Masuk dibatalkan atau kedaluwarsa. Coba lagi.", + "login_error_oauth_failed": "Tidak dapat terhubung ke penyedia. Coba lagi.", + "login_error_oauth_no_email": "Akunmu tidak memiliki alamat email terverifikasi. Tambahkan dan coba lagi.", + + "books_page_title": "Perpustakaan — libnovel", + "books_heading": "Perpustakaanmu", + "books_empty_title": "Belum ada buku", + "books_empty_body": "Tambahkan buku ke perpustakaanmu dengan mengunjungi halaman buku.", + "books_browse_catalogue": "Jelajahi Katalog", + "books_chapter_count": "{n} bab", + "books_last_read": "Terakhir: Bab.{n}", + "books_reading_progress": "Bab.{current} / {total}", + "books_remove": "Hapus", + + "catalogue_page_title": "Katalog — libnovel", + "catalogue_heading": "Katalog", + "catalogue_search_placeholder": "Cari novel…", + "catalogue_filter_genre": "Genre", + "catalogue_filter_status": "Status", + "catalogue_filter_sort": "Urutkan", + "catalogue_sort_popular": "Populer", + "catalogue_sort_new": "Terbaru", + "catalogue_sort_top_rated": "Nilai Tertinggi", + "catalogue_sort_rank": "Peringkat", + "catalogue_status_all": "Semua", + "catalogue_status_ongoing": "Berlangsung", + "catalogue_status_completed": "Selesai", + "catalogue_genre_all": "Semua genre", + "catalogue_clear_filters": "Hapus", + "catalogue_reset": "Atur ulang", + "catalogue_no_results": "Novel tidak ditemukan.", + "catalogue_loading": "Memuat…", + "catalogue_load_more": "Muat lebih banyak", + "catalogue_results_count": "{n} hasil", + + "book_detail_page_title": "{title} — libnovel", + "book_detail_add_to_library": "Tambah ke Perpustakaan", + "book_detail_remove_from_library": "Hapus dari Perpustakaan", + "book_detail_read_now": "Baca Sekarang", + "book_detail_continue_reading": "Lanjutkan Membaca", + "book_detail_start_reading": "Mulai Membaca", + "book_detail_chapters": "{n} Bab", + "book_detail_status": "Status", + "book_detail_author": "Penulis", + "book_detail_genres": "Genre", + "book_detail_description": "Deskripsi", + "book_detail_source": "Sumber", + "book_detail_rescrape": "Perbarui", + "book_detail_scraping": "Memperbarui…", + "book_detail_in_library": "Ada di Perpustakaan", + + "chapters_page_title": "Bab — {title}", + "chapters_heading": "Bab", + "chapters_back_to_book": "Kembali ke buku", + "chapters_reading_now": "Sedang dibaca", + "chapters_empty": "Belum ada bab yang diambil.", + + "reader_page_title": "{title} — Bab.{n} — libnovel", + "reader_play_narration": "Putar narasi", + "reader_generating_audio": "Membuat audio…", + "reader_audio_error": "Pembuatan audio gagal.", + "reader_prev_chapter": "Bab sebelumnya", + "reader_next_chapter": "Bab berikutnya", + "reader_back_to_chapters": "Kembali ke daftar bab", + "reader_chapter_n": "Bab {n}", + "reader_change_voice": "Ganti suara", + "reader_voice_panel_title": "Pilih suara", + "reader_voice_kokoro": "Suara Kokoro", + "reader_voice_pocket": "Suara Pocket-TTS", + "reader_voice_play_sample": "Putar sampel", + "reader_voice_stop_sample": "Hentikan sampel", + "reader_voice_selected": "Dipilih", + "reader_close_voice_panel": "Tutup panel suara", + "reader_auto_next": "Auto-lanjut", + "reader_speed": "Kecepatan", + "reader_preview_notice": "Pratinjau — bab ini belum sepenuhnya diambil.", + + "profile_page_title": "Profil — libnovel", + "profile_heading": "Profil", + "profile_avatar_label": "Avatar", + "profile_change_avatar": "Ubah avatar", + "profile_username": "Nama pengguna", + "profile_email": "Email", + "profile_change_password": "Ubah kata sandi", + "profile_current_password": "Kata sandi saat ini", + "profile_new_password": "Kata sandi baru", + "profile_confirm_password": "Konfirmasi kata sandi", + "profile_save_password": "Simpan kata sandi", + "profile_appearance_heading": "Tampilan", + "profile_theme_label": "Tema", + "profile_theme_amber": "Amber", + "profile_theme_slate": "Abu-abu", + "profile_theme_rose": "Mawar", + "profile_reading_heading": "Pengaturan membaca", + "profile_voice_label": "Suara default", + "profile_speed_label": "Kecepatan pemutaran", + "profile_auto_next_label": "Auto-lanjut bab", + "profile_save_settings": "Simpan pengaturan", + "profile_settings_saved": "Pengaturan disimpan.", + "profile_settings_error": "Gagal menyimpan pengaturan.", + "profile_password_saved": "Kata sandi diubah.", + "profile_password_error": "Gagal mengubah kata sandi.", + "profile_sessions_heading": "Sesi aktif", + "profile_sign_out_all": "Keluar dari semua perangkat lain", + "profile_joined": "Bergabung {date}", + + "user_page_title": "{username} — libnovel", + "user_library_heading": "Perpustakaan {username}", + "user_follow": "Ikuti", + "user_unfollow": "Berhenti mengikuti", + "user_followers": "{n} pengikut", + "user_following": "{n} mengikuti", + "user_library_empty": "Tidak ada buku di perpustakaan.", + + "error_not_found_title": "Halaman tidak ditemukan", + "error_not_found_body": "Halaman yang kamu cari tidak ada.", + "error_generic_title": "Terjadi kesalahan", + "error_go_home": "Ke beranda", + "error_status": "Error {status}", + + "admin_scrape_page_title": "Scrape — Admin", + "admin_scrape_heading": "Scrape", + "admin_scrape_catalogue": "Scrape Katalog", + "admin_scrape_book": "Scrape Buku", + "admin_scrape_url_placeholder": "URL buku di novelfire.net", + "admin_scrape_range": "Rentang bab", + "admin_scrape_from": "Dari", + "admin_scrape_to": "Sampai", + "admin_scrape_submit": "Scrape", + "admin_scrape_cancel": "Batal", + "admin_scrape_status_pending": "Menunggu", + "admin_scrape_status_running": "Berjalan", + "admin_scrape_status_done": "Selesai", + "admin_scrape_status_failed": "Gagal", + "admin_scrape_status_cancelled": "Dibatalkan", + "admin_tasks_heading": "Tugas terbaru", + "admin_tasks_empty": "Belum ada tugas.", + + "admin_audio_page_title": "Audio — Admin", + "admin_audio_heading": "Tugas Audio", + "admin_audio_empty": "Tidak ada tugas audio.", + + "admin_changelog_page_title": "Changelog — Admin", + "admin_changelog_heading": "Changelog", + + "comments_heading": "Komentar", + "comments_empty": "Belum ada komentar. Jadilah yang pertama!", + "comments_placeholder": "Tulis komentar…", + "comments_submit": "Kirim", + "comments_login_prompt": "Masuk untuk berkomentar.", + "comments_vote_up": "Suka", + "comments_vote_down": "Tidak suka", + "comments_delete": "Hapus", + "comments_reply": "Balas", + "comments_show_replies": "Tampilkan {n} balasan", + "comments_hide_replies": "Sembunyikan balasan", + "comments_edited": "diedit", + "comments_deleted": "[dihapus]", + + "disclaimer_page_title": "Penyangkalan — libnovel", + "privacy_page_title": "Kebijakan Privasi — libnovel", + "dmca_page_title": "DMCA — libnovel", + "terms_page_title": "Syarat Layanan — libnovel", + + "common_loading": "Memuat…", + "common_error": "Error", + "common_save": "Simpan", + "common_cancel": "Batal", + "common_close": "Tutup", + "common_search": "Cari", + "common_back": "Kembali", + "common_next": "Berikutnya", + "common_previous": "Sebelumnya", + "common_yes": "Ya", + "common_no": "Tidak", + "common_on": "aktif", + "common_off": "nonaktif", + + "locale_switcher_label": "Bahasa", + + "books_empty_library": "Perpustakaanmu kosong.", + "books_empty_discover": "Buku yang mulai kamu baca atau simpan dari", + "books_empty_discover_link": "Temukan", + "books_empty_discover_suffix": "akan muncul di sini.", + "books_count": "{n} buku", + + "catalogue_sort_updated": "Diperbarui", + "catalogue_search_button": "Cari", + "catalogue_refresh": "Segarkan", + "catalogue_refreshing": "Mengantri…", + "catalogue_refresh_mobile": "Segarkan katalog", + "catalogue_all_loaded": "Semua novel telah dimuat", + "catalogue_scroll_top": "Kembali ke atas", + "catalogue_view_grid": "Tampilan kisi", + "catalogue_view_list": "Tampilan daftar", + "catalogue_browse_source": "Jelajahi novel dari novelfire.net", + "catalogue_search_results": "{n} hasil untuk \"{q}\"", + "catalogue_search_local_count": "({local} lokal, {remote} dari novelfire)", + "catalogue_rank_ranked": "{n} novel diurutkan dari scrape katalog terakhir", + "catalogue_rank_no_data": "Tidak ada data peringkat.", + "catalogue_rank_no_data_body": "Tidak ada data peringkat — jalankan scrape katalog penuh untuk mengisi", + "catalogue_rank_run_scrape_admin": "Klik Segarkan katalog di atas untuk memicu scrape katalog penuh.", + "catalogue_rank_run_scrape_user": "Minta admin untuk menjalankan scrape katalog.", + "catalogue_scrape_queued_flash": "Scrape katalog penuh diantrekan. Perpustakaan dan peringkat akan diperbarui saat buku diproses.", + "catalogue_scrape_busy_flash": "Pekerjaan scrape sedang berjalan. Periksa kembali setelah selesai.", + "catalogue_scrape_error_flash": "Gagal mengantrekan scrape. Pastikan layanan scraper dapat dijangkau.", + "catalogue_filters_label": "Filter", + "catalogue_apply": "Terapkan", + "catalogue_filter_rank_note": "Filter genre & status hanya berlaku untuk Jelajahi", + "catalogue_no_results_search": "Tidak ada hasil.", + "catalogue_no_results_try": "Coba kata kunci lain.", + "catalogue_no_results_filters": "Coba filter lain atau periksa kembali nanti.", + "catalogue_scrape_queued_badge": "Diantrekan", + "catalogue_scrape_busy_badge": "Scraper sibuk", + "catalogue_scrape_busy_list": "Sibuk", + "catalogue_scrape_forbidden_badge": "Terlarang", + "catalogue_scrape_novel_button": "Scrape", + "catalogue_scraping_novel": "Scraping…", + + "book_detail_not_in_library": "tidak di perpustakaan", + "book_detail_continue_ch": "Lanjutkan bab.{n}", + "book_detail_start_ch1": "Mulai dari bab.1", + "book_detail_preview_ch1": "Pratinjau bab.1", + "book_detail_reading_ch": "Membaca bab.{n} dari {total}", + "book_detail_n_chapters": "{n} bab", + "book_detail_rescraping": "Mengantri…", + "book_detail_from_chapter": "Dari bab", + "book_detail_to_chapter": "Sampai bab (opsional)", + "book_detail_range_queuing": "Mengantri…", + "book_detail_scrape_range": "Rentang scrape", + "book_detail_admin": "Admin", + "book_detail_scraping_progress": "Mengambil 20 bab pertama. Halaman ini akan dimuat ulang otomatis.", + "book_detail_scraping_home": "← Beranda", + "book_detail_rescrape_book": "Scrape ulang buku", + "book_detail_less": "Lebih sedikit", + "book_detail_more": "Selengkapnya", + + "chapters_search_placeholder": "Cari bab…", + "chapters_jump_to": "Loncat ke Bab.{n}", + "chapters_no_match": "Tidak ada bab yang cocok dengan \"{q}\"", + "chapters_none_available": "Belum ada bab tersedia.", + "chapters_reading_indicator": "sedang dibaca", + "chapters_result_count": "{n} hasil", + + "reader_fetching_chapter": "Mengambil bab…", + "reader_words": "{n} kata", + "reader_preview_audio_notice": "Pratinjau — audio tidak tersedia untuk buku di luar perpustakaan.", + + "profile_click_to_change": "Klik avatar untuk mengganti foto", + "profile_tts_voice": "Suara TTS", + "profile_auto_advance": "Otomatis lanjut ke bab berikutnya", + "profile_saving": "Menyimpan…", + "profile_saved": "Tersimpan!", + "profile_session_this": "Sesi ini", + "profile_session_signed_in": "Masuk {date}", + "profile_session_last_seen": "· Terakhir dilihat {date}", + "profile_session_sign_out": "Keluar", + "profile_session_end": "Akhiri", + "profile_session_unrecognised": "Ini semua perangkat yang masuk ke akunmu. Akhiri sesi yang tidak kamu kenali.", + "profile_no_sessions": "Tidak ada catatan sesi. Sesi dilacak mulai login berikutnya.", + "profile_change_password_heading": "Ubah kata sandi", + "profile_update_password": "Perbarui kata sandi", + "profile_updating": "Memperbarui…", + "profile_password_changed_ok": "Kata sandi berhasil diubah.", + "profile_playback_speed": "Kecepatan pemutaran — {speed}x", + + "user_currently_reading": "Sedang Dibaca", + "user_library_count": "Perpustakaan ({n})", + "user_joined": "Bergabung {date}", + "user_followers_label": "pengikut", + "user_following_label": "mengikuti", + "user_no_books": "Belum ada buku di perpustakaan.", + + "admin_pages_label": "Halaman", + "admin_tools_label": "Alat", + + "admin_scrape_status_idle": "Menunggu", + "admin_scrape_full_catalogue": "Katalog penuh", + "admin_scrape_single_book": "Satu buku", + "admin_scrape_quick_genres": "Genre cepat", + "admin_scrape_task_history": "Riwayat tugas", + "admin_scrape_filter_placeholder": "Filter berdasarkan jenis, status, atau URL…", + "admin_scrape_no_matching": "Tidak ada tugas yang cocok.", + "admin_scrape_start": "Mulai scrape", + "admin_scrape_queuing": "Mengantri…", + "admin_scrape_running": "Berjalan…", + + "admin_audio_filter_jobs": "Filter berdasarkan slug, suara, atau status…", + "admin_audio_filter_cache": "Filter berdasarkan slug, bab, atau suara…", + "admin_audio_no_matching_jobs": "Tidak ada pekerjaan yang cocok.", + "admin_audio_no_jobs": "Belum ada pekerjaan audio.", + "admin_audio_cache_empty": "Cache audio kosong.", + "admin_audio_no_cache_results": "Tidak ada hasil.", + + "admin_changelog_gitea": "Rilis Gitea", + "admin_changelog_no_releases": "Tidak ada rilis.", + "admin_changelog_load_error": "Gagal memuat rilis: {error}", + + "comments_top": "Teratas", + "comments_new": "Terbaru", + "comments_posting": "Mengirim…", + "comments_login_link": "Masuk", + "comments_login_suffix": "untuk meninggalkan komentar.", + "comments_anonymous": "Anonim", + + "reader_audio_narration": "Narasi Audio", + "reader_playing": "Memutar — kontrol di bawah", + "reader_paused": "Dijeda — kontrol di bawah", + "reader_ch_ready": "Bab.{n} siap", + "reader_ch_preparing": "Menyiapkan Bab.{n}… {percent}%", + "reader_ch_generate_on_nav": "Bab.{n} akan dihasilkan saat navigasi", + "reader_now_playing": "Sedang diputar: {title}", + "reader_load_this_chapter": "Muat bab ini", + "reader_generate_samples": "Hasilkan sampel yang hilang", + "reader_voice_applies_next": "Suara baru berlaku pada \"Putar narasi\" berikutnya.", + "reader_choose_voice": "Pilih Suara", + "reader_generating_narration": "Membuat narasi…" +} diff --git a/ui/messages/pt-BR.json b/ui/messages/pt-BR.json new file mode 100644 index 0000000..f9dd7ff --- /dev/null +++ b/ui/messages/pt-BR.json @@ -0,0 +1,383 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + + "nav_library": "Biblioteca", + "nav_catalogue": "Catálogo", + "nav_feedback": "Feedback", + "nav_admin": "Admin", + "nav_profile": "Perfil", + "nav_sign_in": "Entrar", + "nav_sign_out": "Sair", + "nav_toggle_menu": "Menu", + "nav_admin_panel": "Painel admin", + + "footer_library": "Biblioteca", + "footer_catalogue": "Catálogo", + "footer_feedback": "Feedback", + "footer_disclaimer": "Aviso legal", + "footer_privacy": "Privacidade", + "footer_dmca": "DMCA", + "footer_copyright": "© {year} libnovel", + "footer_dev": "dev", + + "home_title": "libnovel", + "home_stat_books": "Livros", + "home_stat_chapters": "Capítulos", + "home_stat_in_progress": "Em andamento", + "home_continue_reading": "Continuar Lendo", + "home_view_all": "Ver tudo", + "home_recently_updated": "Atualizados Recentemente", + "home_from_following": "De Quem Você Segue", + "home_empty_title": "Sua biblioteca está vazia", + "home_empty_body": "Descubra romances e adicione à sua biblioteca.", + "home_discover_novels": "Descobrir Romances", + "home_via_reader": "via {username}", + "home_chapter_badge": "cap.{n}", + + "player_generating": "Gerando… {percent}%", + "player_loading": "Carregando…", + "player_chapters": "Capítulos", + "player_chapter_n": "Capítulo {n}", + "player_toggle_chapter_list": "Lista de capítulos", + "player_chapter_list_label": "Lista de capítulos", + "player_close_chapter_list": "Fechar lista de capítulos", + "player_rewind_15": "Voltar 15 segundos", + "player_skip_30": "Avançar 30 segundos", + "player_back_15": "−15 s", + "player_forward_30": "+30 s", + "player_play": "Reproduzir", + "player_pause": "Pausar", + "player_speed_label": "Velocidade {speed}x", + "player_change_speed": "Mudar velocidade", + "player_auto_next_on": "Próximo automático ativado", + "player_auto_next_off": "Próximo automático desativado", + "player_auto_next_ready": "Próximo automático — Cap.{n} pronto", + "player_auto_next_preparing": "Próximo automático — preparando Cap.{n}…", + "player_auto_next_aria": "Próximo automático {state}", + "player_go_to_chapter": "Ir para capítulo", + "player_close": "Fechar player", + + "login_page_title": "Entrar — libnovel", + "login_heading": "Entrar no libnovel", + "login_subheading": "Escolha um provedor para continuar", + "login_continue_google": "Continuar com Google", + "login_continue_github": "Continuar com GitHub", + "login_terms_notice": "Ao entrar, você concorda com nossos termos de serviço.", + "login_error_oauth_state": "Login cancelado ou expirado. Tente novamente.", + "login_error_oauth_failed": "Não foi possível conectar ao provedor. Tente novamente.", + "login_error_oauth_no_email": "Sua conta não tem endereço de email verificado. Adicione um e tente novamente.", + + "books_page_title": "Biblioteca — libnovel", + "books_heading": "Sua Biblioteca", + "books_empty_title": "Nenhum livro ainda", + "books_empty_body": "Adicione livros à sua biblioteca visitando a página de um livro.", + "books_browse_catalogue": "Explorar Catálogo", + "books_chapter_count": "{n} capítulos", + "books_last_read": "Último: Cap.{n}", + "books_reading_progress": "Cap.{current} / {total}", + "books_remove": "Remover", + + "catalogue_page_title": "Catálogo — libnovel", + "catalogue_heading": "Catálogo", + "catalogue_search_placeholder": "Pesquisar romances…", + "catalogue_filter_genre": "Gênero", + "catalogue_filter_status": "Status", + "catalogue_filter_sort": "Ordenar", + "catalogue_sort_popular": "Popular", + "catalogue_sort_new": "Novo", + "catalogue_sort_top_rated": "Mais Bem Avaliados", + "catalogue_sort_rank": "Ranking", + "catalogue_status_all": "Todos", + "catalogue_status_ongoing": "Em andamento", + "catalogue_status_completed": "Concluído", + "catalogue_genre_all": "Todos os gêneros", + "catalogue_clear_filters": "Limpar", + "catalogue_reset": "Redefinir", + "catalogue_no_results": "Nenhum romance encontrado.", + "catalogue_loading": "Carregando…", + "catalogue_load_more": "Carregar mais", + "catalogue_results_count": "{n} resultados", + + "book_detail_page_title": "{title} — libnovel", + "book_detail_add_to_library": "Adicionar à Biblioteca", + "book_detail_remove_from_library": "Remover da Biblioteca", + "book_detail_read_now": "Ler Agora", + "book_detail_continue_reading": "Continuar Lendo", + "book_detail_start_reading": "Começar a Ler", + "book_detail_chapters": "{n} Capítulos", + "book_detail_status": "Status", + "book_detail_author": "Autor", + "book_detail_genres": "Gêneros", + "book_detail_description": "Descrição", + "book_detail_source": "Fonte", + "book_detail_rescrape": "Atualizar", + "book_detail_scraping": "Atualizando…", + "book_detail_in_library": "Na Biblioteca", + + "chapters_page_title": "Capítulos — {title}", + "chapters_heading": "Capítulos", + "chapters_back_to_book": "Voltar ao livro", + "chapters_reading_now": "Lendo", + "chapters_empty": "Nenhum capítulo extraído ainda.", + + "reader_page_title": "{title} — Cap.{n} — libnovel", + "reader_play_narration": "Reproduzir narração", + "reader_generating_audio": "Gerando áudio…", + "reader_audio_error": "Falha na geração de áudio.", + "reader_prev_chapter": "Capítulo anterior", + "reader_next_chapter": "Próximo capítulo", + "reader_back_to_chapters": "Voltar aos capítulos", + "reader_chapter_n": "Capítulo {n}", + "reader_change_voice": "Mudar voz", + "reader_voice_panel_title": "Selecionar voz", + "reader_voice_kokoro": "Vozes Kokoro", + "reader_voice_pocket": "Vozes Pocket-TTS", + "reader_voice_play_sample": "Reproduzir amostra", + "reader_voice_stop_sample": "Parar amostra", + "reader_voice_selected": "Selecionado", + "reader_close_voice_panel": "Fechar painel de voz", + "reader_auto_next": "Próximo automático", + "reader_speed": "Velocidade", + "reader_preview_notice": "Prévia — este capítulo não foi totalmente extraído.", + + "profile_page_title": "Perfil — libnovel", + "profile_heading": "Perfil", + "profile_avatar_label": "Avatar", + "profile_change_avatar": "Mudar avatar", + "profile_username": "Nome de usuário", + "profile_email": "Email", + "profile_change_password": "Mudar senha", + "profile_current_password": "Senha atual", + "profile_new_password": "Nova senha", + "profile_confirm_password": "Confirmar senha", + "profile_save_password": "Salvar senha", + "profile_appearance_heading": "Aparência", + "profile_theme_label": "Tema", + "profile_theme_amber": "Âmbar", + "profile_theme_slate": "Ardósia", + "profile_theme_rose": "Rosa", + "profile_reading_heading": "Configurações de leitura", + "profile_voice_label": "Voz padrão", + "profile_speed_label": "Velocidade de reprodução", + "profile_auto_next_label": "Próximo capítulo automático", + "profile_save_settings": "Salvar configurações", + "profile_settings_saved": "Configurações salvas.", + "profile_settings_error": "Falha ao salvar configurações.", + "profile_password_saved": "Senha alterada.", + "profile_password_error": "Falha ao alterar a senha.", + "profile_sessions_heading": "Sessões ativas", + "profile_sign_out_all": "Sair de todos os outros dispositivos", + "profile_joined": "Entrou em {date}", + + "user_page_title": "{username} — libnovel", + "user_library_heading": "Biblioteca de {username}", + "user_follow": "Seguir", + "user_unfollow": "Deixar de seguir", + "user_followers": "{n} seguidores", + "user_following": "{n} seguindo", + "user_library_empty": "Nenhum livro na biblioteca.", + + "error_not_found_title": "Página não encontrada", + "error_not_found_body": "A página que você procura não existe.", + "error_generic_title": "Algo deu errado", + "error_go_home": "Ir para início", + "error_status": "Erro {status}", + + "admin_scrape_page_title": "Extração — Admin", + "admin_scrape_heading": "Extração", + "admin_scrape_catalogue": "Extrair Catálogo", + "admin_scrape_book": "Extrair Livro", + "admin_scrape_url_placeholder": "URL do livro em novelfire.net", + "admin_scrape_range": "Intervalo de capítulos", + "admin_scrape_from": "De", + "admin_scrape_to": "Até", + "admin_scrape_submit": "Extrair", + "admin_scrape_cancel": "Cancelar", + "admin_scrape_status_pending": "Pendente", + "admin_scrape_status_running": "Em execução", + "admin_scrape_status_done": "Concluído", + "admin_scrape_status_failed": "Falhou", + "admin_scrape_status_cancelled": "Cancelado", + "admin_tasks_heading": "Tarefas recentes", + "admin_tasks_empty": "Nenhuma tarefa ainda.", + + "admin_audio_page_title": "Áudio — Admin", + "admin_audio_heading": "Tarefas de Áudio", + "admin_audio_empty": "Nenhuma tarefa de áudio.", + + "admin_changelog_page_title": "Changelog — Admin", + "admin_changelog_heading": "Changelog", + + "comments_heading": "Comentários", + "comments_empty": "Nenhum comentário ainda. Seja o primeiro!", + "comments_placeholder": "Escreva um comentário…", + "comments_submit": "Publicar", + "comments_login_prompt": "Entre para comentar.", + "comments_vote_up": "Votar positivo", + "comments_vote_down": "Votar negativo", + "comments_delete": "Excluir", + "comments_reply": "Responder", + "comments_show_replies": "Mostrar {n} respostas", + "comments_hide_replies": "Ocultar respostas", + "comments_edited": "editado", + "comments_deleted": "[excluído]", + + "disclaimer_page_title": "Aviso Legal — libnovel", + "privacy_page_title": "Política de Privacidade — libnovel", + "dmca_page_title": "DMCA — libnovel", + "terms_page_title": "Termos de Serviço — libnovel", + + "common_loading": "Carregando…", + "common_error": "Erro", + "common_save": "Salvar", + "common_cancel": "Cancelar", + "common_close": "Fechar", + "common_search": "Pesquisar", + "common_back": "Voltar", + "common_next": "Próximo", + "common_previous": "Anterior", + "common_yes": "Sim", + "common_no": "Não", + "common_on": "ativado", + "common_off": "desativado", + + "locale_switcher_label": "Idioma", + + "books_empty_library": "Sua biblioteca está vazia.", + "books_empty_discover": "Livros que você começar a ler ou salvar de", + "books_empty_discover_link": "Descobrir", + "books_empty_discover_suffix": "aparecerão aqui.", + "books_count": "{n} livro{s}", + + "catalogue_sort_updated": "Atualizado", + "catalogue_search_button": "Pesquisar", + "catalogue_refresh": "Atualizar", + "catalogue_refreshing": "Na fila…", + "catalogue_refresh_mobile": "Atualizar catálogo", + "catalogue_all_loaded": "Todos os romances carregados", + "catalogue_scroll_top": "Voltar ao topo", + "catalogue_view_grid": "Visualização em grade", + "catalogue_view_list": "Visualização em lista", + "catalogue_browse_source": "Explorar romances do novelfire.net", + "catalogue_search_results": "{n} resultado{s} para \"{q}\"", + "catalogue_search_local_count": "({local} local, {remote} do novelfire)", + "catalogue_rank_ranked": "{n} romances classificados do último scrape do catálogo", + "catalogue_rank_no_data": "Sem dados de classificação.", + "catalogue_rank_no_data_body": "Sem dados de classificação — execute um scrape completo do catálogo para preencher", + "catalogue_rank_run_scrape_admin": "Clique em Atualizar catálogo acima para acionar um scrape completo.", + "catalogue_rank_run_scrape_user": "Peça a um administrador para executar um scrape do catálogo.", + "catalogue_scrape_queued_flash": "Scrape completo do catálogo na fila. A biblioteca e a classificação serão atualizadas conforme os livros forem processados.", + "catalogue_scrape_busy_flash": "Um job de scrape já está em execução. Volte quando terminar.", + "catalogue_scrape_error_flash": "Falha ao enfileirar o scrape. Verifique se o serviço de scraper está acessível.", + "catalogue_filters_label": "Filtros", + "catalogue_apply": "Aplicar", + "catalogue_filter_rank_note": "Filtros de gênero e status se aplicam apenas a Explorar", + "catalogue_no_results_search": "Nenhum resultado encontrado.", + "catalogue_no_results_try": "Tente um termo de pesquisa diferente.", + "catalogue_no_results_filters": "Tente filtros diferentes ou volte mais tarde.", + "catalogue_scrape_queued_badge": "Na fila", + "catalogue_scrape_busy_badge": "Scraper ocupado", + "catalogue_scrape_busy_list": "Ocupado", + "catalogue_scrape_forbidden_badge": "Proibido", + "catalogue_scrape_novel_button": "Extrair", + "catalogue_scraping_novel": "Extraindo…", + + "book_detail_not_in_library": "não está na biblioteca", + "book_detail_continue_ch": "Continuar cap.{n}", + "book_detail_start_ch1": "Começar pelo cap.1", + "book_detail_preview_ch1": "Prévia do cap.1", + "book_detail_reading_ch": "Lendo cap.{n} de {total}", + "book_detail_n_chapters": "{n} capítulos", + "book_detail_rescraping": "Na fila…", + "book_detail_from_chapter": "A partir do capítulo", + "book_detail_to_chapter": "Até o capítulo (opcional)", + "book_detail_range_queuing": "Na fila…", + "book_detail_scrape_range": "Intervalo de extração", + "book_detail_admin": "Admin", + "book_detail_scraping_progress": "Buscando os primeiros 20 capítulos. Esta página será atualizada automaticamente.", + "book_detail_scraping_home": "← Início", + "book_detail_rescrape_book": "Reextrair livro", + "book_detail_less": "Menos", + "book_detail_more": "Mais", + + "chapters_search_placeholder": "Pesquisar capítulos…", + "chapters_jump_to": "Ir para Cap.{n}", + "chapters_no_match": "Nenhum capítulo encontrado para \"{q}\"", + "chapters_none_available": "Nenhum capítulo disponível ainda.", + "chapters_reading_indicator": "lendo", + "chapters_result_count": "{n} resultados", + + "reader_fetching_chapter": "Buscando capítulo…", + "reader_words": "{n} palavras", + "reader_preview_audio_notice": "Prévia — áudio não disponível para livros fora da biblioteca.", + + "profile_click_to_change": "Clique no avatar para mudar a foto", + "profile_tts_voice": "Voz TTS", + "profile_auto_advance": "Avançar automaticamente para o próximo capítulo", + "profile_saving": "Salvando…", + "profile_saved": "Salvo!", + "profile_session_this": "Esta sessão", + "profile_session_signed_in": "Entrou em {date}", + "profile_session_last_seen": "· Visto por último em {date}", + "profile_session_sign_out": "Sair", + "profile_session_end": "Encerrar", + "profile_session_unrecognised": "Estes são todos os dispositivos conectados à sua conta. Encerre qualquer sessão que não reconhecer.", + "profile_no_sessions": "Nenhum registro de sessão encontrado. As sessões são rastreadas a partir do próximo login.", + "profile_change_password_heading": "Mudar senha", + "profile_update_password": "Atualizar senha", + "profile_updating": "Atualizando…", + "profile_password_changed_ok": "Senha alterada com sucesso.", + "profile_playback_speed": "Velocidade de reprodução — {speed}x", + + "user_currently_reading": "Lendo Agora", + "user_library_count": "Biblioteca ({n})", + "user_joined": "Entrou em {date}", + "user_followers_label": "seguidores", + "user_following_label": "seguindo", + "user_no_books": "Nenhum livro na biblioteca ainda.", + + "admin_pages_label": "Páginas", + "admin_tools_label": "Ferramentas", + + "admin_scrape_status_idle": "Ocioso", + "admin_scrape_full_catalogue": "Catálogo completo", + "admin_scrape_single_book": "Livro único", + "admin_scrape_quick_genres": "Gêneros rápidos", + "admin_scrape_task_history": "Histórico de tarefas", + "admin_scrape_filter_placeholder": "Filtrar por tipo, status ou URL…", + "admin_scrape_no_matching": "Nenhuma tarefa correspondente.", + "admin_scrape_start": "Iniciar extração", + "admin_scrape_queuing": "Na fila…", + "admin_scrape_running": "Executando…", + + "admin_audio_filter_jobs": "Filtrar por slug, voz ou status…", + "admin_audio_filter_cache": "Filtrar por slug, capítulo ou voz…", + "admin_audio_no_matching_jobs": "Nenhum job correspondente.", + "admin_audio_no_jobs": "Nenhum job de áudio ainda.", + "admin_audio_cache_empty": "Cache de áudio vazio.", + "admin_audio_no_cache_results": "Sem resultados.", + + "admin_changelog_gitea": "Releases do Gitea", + "admin_changelog_no_releases": "Nenhum release encontrado.", + "admin_changelog_load_error": "Não foi possível carregar os releases: {error}", + + "comments_top": "Mais votados", + "comments_new": "Novos", + "comments_posting": "Publicando…", + "comments_login_link": "Entre", + "comments_login_suffix": "para deixar um comentário.", + "comments_anonymous": "Anônimo", + + "reader_audio_narration": "Narração em Áudio", + "reader_playing": "Reproduzindo — controles abaixo", + "reader_paused": "Pausado — controles abaixo", + "reader_ch_ready": "Cap.{n} pronto", + "reader_ch_preparing": "Preparando Cap.{n}… {percent}%", + "reader_ch_generate_on_nav": "Cap.{n} será gerado ao navegar", + "reader_now_playing": "Reproduzindo: {title}", + "reader_load_this_chapter": "Carregar este capítulo", + "reader_generate_samples": "Gerar amostras ausentes", + "reader_voice_applies_next": "A nova voz será aplicada no próximo \"Reproduzir narração\".", + "reader_choose_voice": "Escolher Voz", + "reader_generating_narration": "Gerando narração…" +} diff --git a/ui/messages/ru.json b/ui/messages/ru.json new file mode 100644 index 0000000..1003684 --- /dev/null +++ b/ui/messages/ru.json @@ -0,0 +1,383 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + + "nav_library": "Библиотека", + "nav_catalogue": "Каталог", + "nav_feedback": "Обратная связь", + "nav_admin": "Админ", + "nav_profile": "Профиль", + "nav_sign_in": "Войти", + "nav_sign_out": "Выйти", + "nav_toggle_menu": "Меню", + "nav_admin_panel": "Панель администратора", + + "footer_library": "Библиотека", + "footer_catalogue": "Каталог", + "footer_feedback": "Обратная связь", + "footer_disclaimer": "Отказ от ответственности", + "footer_privacy": "Конфиденциальность", + "footer_dmca": "DMCA", + "footer_copyright": "© {year} libnovel", + "footer_dev": "dev", + + "home_title": "libnovel", + "home_stat_books": "Книги", + "home_stat_chapters": "Главы", + "home_stat_in_progress": "В процессе", + "home_continue_reading": "Продолжить чтение", + "home_view_all": "Смотреть все", + "home_recently_updated": "Недавно обновлённые", + "home_from_following": "От авторов, на которых вы подписаны", + "home_empty_title": "Ваша библиотека пуста", + "home_empty_body": "Откройте для себя новеллы и добавьте их в библиотеку.", + "home_discover_novels": "Открыть новеллы", + "home_via_reader": "от {username}", + "home_chapter_badge": "гл.{n}", + + "player_generating": "Генерация… {percent}%", + "player_loading": "Загрузка…", + "player_chapters": "Главы", + "player_chapter_n": "Глава {n}", + "player_toggle_chapter_list": "Список глав", + "player_chapter_list_label": "Список глав", + "player_close_chapter_list": "Закрыть список глав", + "player_rewind_15": "Назад 15 секунд", + "player_skip_30": "Вперёд 30 секунд", + "player_back_15": "−15 сек", + "player_forward_30": "+30 сек", + "player_play": "Воспроизвести", + "player_pause": "Пауза", + "player_speed_label": "Скорость {speed}x", + "player_change_speed": "Изменить скорость", + "player_auto_next_on": "Автопереход вкл.", + "player_auto_next_off": "Автопереход выкл.", + "player_auto_next_ready": "Автопереход — гл.{n} готова", + "player_auto_next_preparing": "Автопереход — подготовка гл.{n}…", + "player_auto_next_aria": "Автопереход {state}", + "player_go_to_chapter": "Перейти к главе", + "player_close": "Закрыть плеер", + + "login_page_title": "Вход — libnovel", + "login_heading": "Войти в libnovel", + "login_subheading": "Выберите провайдера для входа", + "login_continue_google": "Продолжить с Google", + "login_continue_github": "Продолжить с GitHub", + "login_terms_notice": "Входя, вы принимаете наши условия использования.", + "login_error_oauth_state": "Вход отменён или истёк срок действия. Попробуйте снова.", + "login_error_oauth_failed": "Не удалось подключиться к провайдеру. Попробуйте снова.", + "login_error_oauth_no_email": "У вашего аккаунта нет подтверждённого email. Добавьте его и повторите попытку.", + + "books_page_title": "Библиотека — libnovel", + "books_heading": "Ваша библиотека", + "books_empty_title": "Книг пока нет", + "books_empty_body": "Добавляйте книги в библиотеку, посещая страницы книг.", + "books_browse_catalogue": "Обзор каталога", + "books_chapter_count": "{n} глав", + "books_last_read": "Последнее: гл.{n}", + "books_reading_progress": "Гл.{current} / {total}", + "books_remove": "Удалить", + + "catalogue_page_title": "Каталог — libnovel", + "catalogue_heading": "Каталог", + "catalogue_search_placeholder": "Поиск новелл…", + "catalogue_filter_genre": "Жанр", + "catalogue_filter_status": "Статус", + "catalogue_filter_sort": "Сортировка", + "catalogue_sort_popular": "Популярные", + "catalogue_sort_new": "Новые", + "catalogue_sort_top_rated": "Топ по рейтингу", + "catalogue_sort_rank": "По рангу", + "catalogue_status_all": "Все", + "catalogue_status_ongoing": "Продолжаются", + "catalogue_status_completed": "Завершены", + "catalogue_genre_all": "Все жанры", + "catalogue_clear_filters": "Сбросить", + "catalogue_reset": "Сброс", + "catalogue_no_results": "Новеллы не найдены.", + "catalogue_loading": "Загрузка…", + "catalogue_load_more": "Загрузить ещё", + "catalogue_results_count": "{n} результатов", + + "book_detail_page_title": "{title} — libnovel", + "book_detail_add_to_library": "В библиотеку", + "book_detail_remove_from_library": "Удалить из библиотеки", + "book_detail_read_now": "Читать", + "book_detail_continue_reading": "Продолжить чтение", + "book_detail_start_reading": "Начать чтение", + "book_detail_chapters": "{n} глав", + "book_detail_status": "Статус", + "book_detail_author": "Автор", + "book_detail_genres": "Жанры", + "book_detail_description": "Описание", + "book_detail_source": "Источник", + "book_detail_rescrape": "Обновить", + "book_detail_scraping": "Обновление…", + "book_detail_in_library": "В библиотеке", + + "chapters_page_title": "Главы — {title}", + "chapters_heading": "Главы", + "chapters_back_to_book": "К книге", + "chapters_reading_now": "Читается", + "chapters_empty": "Главы ещё не загружены.", + + "reader_page_title": "{title} — Гл.{n} — libnovel", + "reader_play_narration": "Воспроизвести озвучку", + "reader_generating_audio": "Генерация аудио…", + "reader_audio_error": "Ошибка генерации аудио.", + "reader_prev_chapter": "Предыдущая глава", + "reader_next_chapter": "Следующая глава", + "reader_back_to_chapters": "К главам", + "reader_chapter_n": "Глава {n}", + "reader_change_voice": "Сменить голос", + "reader_voice_panel_title": "Выбрать голос", + "reader_voice_kokoro": "Голоса Kokoro", + "reader_voice_pocket": "Голоса Pocket-TTS", + "reader_voice_play_sample": "Прослушать образец", + "reader_voice_stop_sample": "Остановить образец", + "reader_voice_selected": "Выбран", + "reader_close_voice_panel": "Закрыть панель голоса", + "reader_auto_next": "Автопереход", + "reader_speed": "Скорость", + "reader_preview_notice": "Предпросмотр — эта глава не полностью загружена.", + + "profile_page_title": "Профиль — libnovel", + "profile_heading": "Профиль", + "profile_avatar_label": "Аватар", + "profile_change_avatar": "Изменить аватар", + "profile_username": "Имя пользователя", + "profile_email": "Email", + "profile_change_password": "Изменить пароль", + "profile_current_password": "Текущий пароль", + "profile_new_password": "Новый пароль", + "profile_confirm_password": "Подтвердить пароль", + "profile_save_password": "Сохранить пароль", + "profile_appearance_heading": "Внешний вид", + "profile_theme_label": "Тема", + "profile_theme_amber": "Янтарь", + "profile_theme_slate": "Сланец", + "profile_theme_rose": "Роза", + "profile_reading_heading": "Настройки чтения", + "profile_voice_label": "Голос по умолчанию", + "profile_speed_label": "Скорость воспроизведения", + "profile_auto_next_label": "Автопереход к следующей главе", + "profile_save_settings": "Сохранить настройки", + "profile_settings_saved": "Настройки сохранены.", + "profile_settings_error": "Не удалось сохранить настройки.", + "profile_password_saved": "Пароль изменён.", + "profile_password_error": "Не удалось изменить пароль.", + "profile_sessions_heading": "Активные сессии", + "profile_sign_out_all": "Выйти на всех других устройствах", + "profile_joined": "Зарегистрирован {date}", + + "user_page_title": "{username} — libnovel", + "user_library_heading": "Библиотека {username}", + "user_follow": "Подписаться", + "user_unfollow": "Отписаться", + "user_followers": "{n} подписчиков", + "user_following": "{n} подписок", + "user_library_empty": "В библиотеке нет книг.", + + "error_not_found_title": "Страница не найдена", + "error_not_found_body": "Запрошенная страница не существует.", + "error_generic_title": "Что-то пошло не так", + "error_go_home": "На главную", + "error_status": "Ошибка {status}", + + "admin_scrape_page_title": "Парсинг — Админ", + "admin_scrape_heading": "Парсинг", + "admin_scrape_catalogue": "Парсинг каталога", + "admin_scrape_book": "Парсинг книги", + "admin_scrape_url_placeholder": "URL книги на novelfire.net", + "admin_scrape_range": "Диапазон глав", + "admin_scrape_from": "От", + "admin_scrape_to": "До", + "admin_scrape_submit": "Парсить", + "admin_scrape_cancel": "Отмена", + "admin_scrape_status_pending": "Ожидание", + "admin_scrape_status_running": "Выполняется", + "admin_scrape_status_done": "Готово", + "admin_scrape_status_failed": "Ошибка", + "admin_scrape_status_cancelled": "Отменено", + "admin_tasks_heading": "Последние задачи", + "admin_tasks_empty": "Задач пока нет.", + + "admin_audio_page_title": "Аудио — Админ", + "admin_audio_heading": "Аудио задачи", + "admin_audio_empty": "Аудио задач нет.", + + "admin_changelog_page_title": "Changelog — Админ", + "admin_changelog_heading": "Changelog", + + "comments_heading": "Комментарии", + "comments_empty": "Комментариев пока нет. Будьте первым!", + "comments_placeholder": "Написать комментарий…", + "comments_submit": "Отправить", + "comments_login_prompt": "Войдите, чтобы комментировать.", + "comments_vote_up": "Плюс", + "comments_vote_down": "Минус", + "comments_delete": "Удалить", + "comments_reply": "Ответить", + "comments_show_replies": "Показать {n} ответов", + "comments_hide_replies": "Скрыть ответы", + "comments_edited": "изменено", + "comments_deleted": "[удалено]", + + "disclaimer_page_title": "Отказ от ответственности — libnovel", + "privacy_page_title": "Политика конфиденциальности — libnovel", + "dmca_page_title": "DMCA — libnovel", + "terms_page_title": "Условия использования — libnovel", + + "common_loading": "Загрузка…", + "common_error": "Ошибка", + "common_save": "Сохранить", + "common_cancel": "Отмена", + "common_close": "Закрыть", + "common_search": "Поиск", + "common_back": "Назад", + "common_next": "Далее", + "common_previous": "Назад", + "common_yes": "Да", + "common_no": "Нет", + "common_on": "вкл.", + "common_off": "выкл.", + + "locale_switcher_label": "Язык", + + "books_empty_library": "Ваша библиотека пуста.", + "books_empty_discover": "Книги, которые вы начнёте читать или сохраните из", + "books_empty_discover_link": "Каталога", + "books_empty_discover_suffix": "появятся здесь.", + "books_count": "{n} книг{s}", + + "catalogue_sort_updated": "По дате обновления", + "catalogue_search_button": "Поиск", + "catalogue_refresh": "Обновить", + "catalogue_refreshing": "В очереди…", + "catalogue_refresh_mobile": "Обновить каталог", + "catalogue_all_loaded": "Все новеллы загружены", + "catalogue_scroll_top": "Вверх", + "catalogue_view_grid": "Сетка", + "catalogue_view_list": "Список", + "catalogue_browse_source": "Смотреть новеллы с novelfire.net", + "catalogue_search_results": "{n} результат{s} по запросу «{q}»", + "catalogue_search_local_count": "({local} локальных, {remote} с novelfire)", + "catalogue_rank_ranked": "{n} новелл отсортированы по последнему парсингу каталога", + "catalogue_rank_no_data": "Нет данных рейтинга.", + "catalogue_rank_no_data_body": "Нет данных рейтинга — запустите полный парсинг каталога для заполнения", + "catalogue_rank_run_scrape_admin": "Нажмите «Обновить каталог» выше, чтобы запустить полный парсинг.", + "catalogue_rank_run_scrape_user": "Попросите администратора запустить парсинг каталога.", + "catalogue_scrape_queued_flash": "Полный парсинг каталога поставлен в очередь. Библиотека и рейтинг обновятся по мере обработки.", + "catalogue_scrape_busy_flash": "Парсинг уже запущен. Проверьте позже.", + "catalogue_scrape_error_flash": "Не удалось поставить парсинг в очередь. Проверьте доступность сервиса.", + "catalogue_filters_label": "Фильтры", + "catalogue_apply": "Применить", + "catalogue_filter_rank_note": "Фильтры по жанру и статусу применяются только к разделу «Обзор»", + "catalogue_no_results_search": "Ничего не найдено.", + "catalogue_no_results_try": "Попробуйте другой запрос.", + "catalogue_no_results_filters": "Попробуйте другие фильтры или проверьте позже.", + "catalogue_scrape_queued_badge": "В очереди", + "catalogue_scrape_busy_badge": "Парсер занят", + "catalogue_scrape_busy_list": "Занят", + "catalogue_scrape_forbidden_badge": "Запрещено", + "catalogue_scrape_novel_button": "Парсить", + "catalogue_scraping_novel": "Парсинг…", + + "book_detail_not_in_library": "не в библиотеке", + "book_detail_continue_ch": "Продолжить гл.{n}", + "book_detail_start_ch1": "Начать с гл.1", + "book_detail_preview_ch1": "Предпросмотр гл.1", + "book_detail_reading_ch": "Читается гл.{n} из {total}", + "book_detail_n_chapters": "{n} глав", + "book_detail_rescraping": "В очереди…", + "book_detail_from_chapter": "С главы", + "book_detail_to_chapter": "До главы (необязательно)", + "book_detail_range_queuing": "В очереди…", + "book_detail_scrape_range": "Диапазон глав", + "book_detail_admin": "Администрирование", + "book_detail_scraping_progress": "Загружаются первые 20 глав. Страница обновится автоматически.", + "book_detail_scraping_home": "← На главную", + "book_detail_rescrape_book": "Перепарсить книгу", + "book_detail_less": "Скрыть", + "book_detail_more": "Ещё", + + "chapters_search_placeholder": "Поиск глав…", + "chapters_jump_to": "Перейти к гл.{n}", + "chapters_no_match": "Главы по запросу «{q}» не найдены", + "chapters_none_available": "Глав пока нет.", + "chapters_reading_indicator": "читается", + "chapters_result_count": "{n} результатов", + + "reader_fetching_chapter": "Загрузка главы…", + "reader_words": "{n} слов", + "reader_preview_audio_notice": "Предпросмотр — аудио недоступно для книг вне библиотеки.", + + "profile_click_to_change": "Нажмите на аватар для смены фото", + "profile_tts_voice": "Голос TTS", + "profile_auto_advance": "Автопереход к следующей главе", + "profile_saving": "Сохранение…", + "profile_saved": "Сохранено!", + "profile_session_this": "Текущая сессия", + "profile_session_signed_in": "Вход {date}", + "profile_session_last_seen": "· Последний визит {date}", + "profile_session_sign_out": "Выйти", + "profile_session_end": "Завершить", + "profile_session_unrecognised": "Это все устройства, авторизованные в вашем аккаунте. Завершите любую сессию, которую не узнаёте.", + "profile_no_sessions": "Записей сессий нет. Отслеживание начнётся со следующего входа.", + "profile_change_password_heading": "Изменить пароль", + "profile_update_password": "Обновить пароль", + "profile_updating": "Обновление…", + "profile_password_changed_ok": "Пароль успешно изменён.", + "profile_playback_speed": "Скорость воспроизведения — {speed}x", + + "user_currently_reading": "Сейчас читает", + "user_library_count": "Библиотека ({n})", + "user_joined": "Зарегистрирован {date}", + "user_followers_label": "подписчиков", + "user_following_label": "подписок", + "user_no_books": "Книг в библиотеке пока нет.", + + "admin_pages_label": "Страницы", + "admin_tools_label": "Инструменты", + + "admin_scrape_status_idle": "Ожидание", + "admin_scrape_full_catalogue": "Полный каталог", + "admin_scrape_single_book": "Одна книга", + "admin_scrape_quick_genres": "Быстрые жанры", + "admin_scrape_task_history": "История задач", + "admin_scrape_filter_placeholder": "Фильтр по типу, статусу или URL…", + "admin_scrape_no_matching": "Задач не найдено.", + "admin_scrape_start": "Начать парсинг", + "admin_scrape_queuing": "В очереди…", + "admin_scrape_running": "Выполняется…", + + "admin_audio_filter_jobs": "Фильтр по slug, голосу или статусу…", + "admin_audio_filter_cache": "Фильтр по slug, главе или голосу…", + "admin_audio_no_matching_jobs": "Заданий не найдено.", + "admin_audio_no_jobs": "Аудиозаданий пока нет.", + "admin_audio_cache_empty": "Аудиокэш пуст.", + "admin_audio_no_cache_results": "Результатов нет.", + + "admin_changelog_gitea": "Релизы Gitea", + "admin_changelog_no_releases": "Релизов не найдено.", + "admin_changelog_load_error": "Не удалось загрузить релизы: {error}", + + "comments_top": "Лучшие", + "comments_new": "Новые", + "comments_posting": "Отправка…", + "comments_login_link": "Войдите", + "comments_login_suffix": "чтобы оставить комментарий.", + "comments_anonymous": "Аноним", + + "reader_audio_narration": "Аудионарратив", + "reader_playing": "Воспроизводится — управление ниже", + "reader_paused": "Пауза — управление ниже", + "reader_ch_ready": "Гл.{n} готова", + "reader_ch_preparing": "Подготовка гл.{n}… {percent}%", + "reader_ch_generate_on_nav": "Гл.{n} сгенерируется при переходе", + "reader_now_playing": "Сейчас играет: {title}", + "reader_load_this_chapter": "Загрузить эту главу", + "reader_generate_samples": "Сгенерировать недостающие образцы", + "reader_voice_applies_next": "Новый голос применится при следующем нажатии «Воспроизвести».", + "reader_choose_voice": "Выбрать голос", + "reader_generating_narration": "Генерация озвучки…" +} diff --git a/ui/package-lock.json b/ui/package-lock.json index 738c7d3..c5d54ba 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.1005.0", "@aws-sdk/s3-request-presigner": "^3.1005.0", + "@inlang/paraglide-js": "^2.15.1", "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", "@opentelemetry/resources": "^2.6.1", @@ -1719,6 +1720,49 @@ "node": ">=6" } }, + "node_modules/@inlang/paraglide-js": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/@inlang/paraglide-js/-/paraglide-js-2.15.1.tgz", + "integrity": "sha512-7wWKbLWwLx1dkkYz55TnVp+39atKXf7rnlHnL8adSmM73UaAdB9fXDzo24GHSY/6FPGFKSkgHdT2qyJv2whWsA==", + "license": "MIT", + "dependencies": { + "@inlang/recommend-sherlock": "^0.2.1", + "@inlang/sdk": "^2.9.1", + "commander": "11.1.0", + "consola": "3.4.0", + "json5": "2.2.3", + "unplugin": "^2.1.2", + "urlpattern-polyfill": "^10.0.0" + }, + "bin": { + "paraglide-js": "bin/run.js" + } + }, + "node_modules/@inlang/recommend-sherlock": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@inlang/recommend-sherlock/-/recommend-sherlock-0.2.1.tgz", + "integrity": "sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==", + "license": "MIT", + "dependencies": { + "comment-json": "^4.2.3" + } + }, + "node_modules/@inlang/sdk": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@inlang/sdk/-/sdk-2.9.1.tgz", + "integrity": "sha512-y0C3xaKo6pSGDr3p5OdreRVT3THJpgKVe1lLvG3BE4v9lskp3UfI9cPCbN8X2dpfLt/4ljtehMb5SykpMfJrMg==", + "license": "MIT", + "dependencies": { + "@lix-js/sdk": "0.4.9", + "@sinclair/typebox": "^0.31.17", + "kysely": "^0.28.12", + "sqlite-wasm-kysely": "0.3.0", + "uuid": "^13.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@ioredis/commands": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", @@ -1780,6 +1824,43 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@lix-js/sdk": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@lix-js/sdk/-/sdk-0.4.9.tgz", + "integrity": "sha512-30mDkXpx704359oRrJI42bjfCspCiaMItngVBbPkiTGypS7xX4jYbHWQkXI8XuJ7VDB69D0MsVU6xfrBAIrM4A==", + "license": "Apache-2.0", + "dependencies": { + "@lix-js/server-protocol-schema": "0.1.1", + "dedent": "1.5.1", + "human-id": "^4.1.1", + "js-sha256": "^0.11.0", + "kysely": "^0.28.12", + "sqlite-wasm-kysely": "0.3.0", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@lix-js/sdk/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@lix-js/server-protocol-schema": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@lix-js/server-protocol-schema/-/server-protocol-schema-0.1.1.tgz", + "integrity": "sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==", + "license": "Apache-2.0" + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -4135,6 +4216,12 @@ "node": ">= 18" } }, + "node_modules/@sinclair/typebox": { + "version": "0.31.28", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.31.28.tgz", + "integrity": "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==", + "license": "MIT" + }, "node_modules/@smithy/abort-controller": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.11.tgz", @@ -4867,6 +4954,15 @@ "node": ">=18.0.0" } }, + "node_modules/@sqlite.org/sqlite-wasm": { + "version": "3.48.0-build4", + "resolved": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.48.0-build4.tgz", + "integrity": "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==", + "license": "Apache-2.0", + "bin": { + "sqlite-wasm": "bin/index.js" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -5405,6 +5501,12 @@ "node": ">= 0.4" } }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "license": "MIT" + }, "node_modules/ast-types": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", @@ -5590,6 +5692,28 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/comment-json": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.6.2.tgz", + "integrity": "sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==", + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -5597,6 +5721,15 @@ "dev": true, "license": "MIT" }, + "node_modules/consola": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz", + "integrity": "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -5635,6 +5768,20 @@ } } }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -5966,6 +6113,15 @@ "node": ">= 6" } }, + "node_modules/human-id": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.3.tgz", + "integrity": "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==", + "license": "MIT", + "bin": { + "human-id": "dist/cli.js" + } + }, "node_modules/import-in-the-middle": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.0.tgz", @@ -6062,6 +6218,12 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-sha256": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz", + "integrity": "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6101,6 +6263,15 @@ "node": ">=6" } }, + "node_modules/kysely": { + "version": "0.28.14", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.14.tgz", + "integrity": "sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/lightningcss": { "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", @@ -6988,6 +7159,17 @@ "node": ">=0.10.0" } }, + "node_modules/sqlite-wasm-kysely": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/sqlite-wasm-kysely/-/sqlite-wasm-kysely-0.3.0.tgz", + "integrity": "sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==", + "dependencies": { + "@sqlite.org/sqlite-wasm": "^3.48.0-build2" + }, + "peerDependencies": { + "kysely": "*" + } + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -7201,6 +7383,21 @@ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -7231,6 +7428,25 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -7330,6 +7546,12 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/ui/package.json b/ui/package.json index c7c8a43..b370e84 100644 --- a/ui/package.json +++ b/ui/package.json @@ -30,6 +30,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.1005.0", "@aws-sdk/s3-request-presigner": "^3.1005.0", + "@inlang/paraglide-js": "^2.15.1", "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", "@opentelemetry/resources": "^2.6.1", diff --git a/ui/project.inlang/settings.json b/ui/project.inlang/settings.json new file mode 100644 index 0000000..e79a8d1 --- /dev/null +++ b/ui/project.inlang/settings.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://inlang.com/schema/project-settings", + "baseLocale": "en", + "locales": ["en", "ru", "id", "pt-BR", "fr"], + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format/dist/index.js" + ], + "plugin.inlang.messageFormat": { + "pathPattern": "./messages/{locale}.json" + } +} diff --git a/ui/src/app.html b/ui/src/app.html index c1b5e52..3cef78f 100644 --- a/ui/src/app.html +++ b/ui/src/app.html @@ -1,5 +1,5 @@ - + diff --git a/ui/src/hooks.server.ts b/ui/src/hooks.server.ts index cc3cafb..a249658 100644 --- a/ui/src/hooks.server.ts +++ b/ui/src/hooks.server.ts @@ -1,4 +1,5 @@ import type { Handle } from '@sveltejs/kit'; +import { sequence } from '@sveltejs/kit/hooks'; import { handleErrorWithSentry } from '@sentry/sveltekit'; import * as Sentry from '@sentry/sveltekit'; import { randomBytes, createHmac } from 'node:crypto'; @@ -13,6 +14,7 @@ import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs'; import { resourceFromAttributes } from '@opentelemetry/resources'; import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'; +import { paraglideMiddleware } from '$lib/paraglide/server'; // ─── OpenTelemetry server-side tracing + logs ───────────────────────────────── // No-op when OTEL_EXPORTER_OTLP_ENDPOINT is unset (e.g. local dev). @@ -138,7 +140,21 @@ export function parseAuthToken(token: string): { id: string; username: string; r // ─── Hook ───────────────────────────────────────────────────────────────────── -export const handle: Handle = async ({ event, resolve }) => { +function getTextDirection(locale: string): string { + // All supported locales (en, ru, id, pt-BR, fr) are LTR + return 'ltr'; +} + +const paraglideHandle: Handle = ({ event, resolve }) => + paraglideMiddleware(event.request, ({ request: localizedRequest, locale }) => { + event.request = localizedRequest; + return resolve(event, { + transformPageChunk: ({ html }) => + html.replace('%lang%', locale).replace('%dir%', getTextDirection(locale)) + }); + }); + +const appHandle: Handle = async ({ event, resolve }) => { // During graceful shutdown, reject new requests immediately so the load // balancer / Docker health-check can drain existing connections. if (shuttingDown) { @@ -197,3 +213,5 @@ export const handle: Handle = async ({ event, resolve }) => { return resolve(event); }; +export const handle = sequence(paraglideHandle, appHandle); + diff --git a/ui/src/hooks.ts b/ui/src/hooks.ts new file mode 100644 index 0000000..7e63924 --- /dev/null +++ b/ui/src/hooks.ts @@ -0,0 +1,4 @@ +import type { Reroute } from '@sveltejs/kit'; +import { deLocalizeUrl } from '$lib/paraglide/runtime'; + +export const reroute: Reroute = ({ url }) => deLocalizeUrl(url).pathname; diff --git a/ui/src/lib/components/AudioPlayer.svelte b/ui/src/lib/components/AudioPlayer.svelte index e0b1061..e90ad39 100644 --- a/ui/src/lib/components/AudioPlayer.svelte +++ b/ui/src/lib/components/AudioPlayer.svelte @@ -52,6 +52,7 @@ import { Button } from '$lib/components/ui/button'; import { cn } from '$lib/utils'; import type { Voice } from '$lib/types'; + import * as m from '$lib/paraglide/messages.js'; interface Props { slug: string; @@ -702,7 +703,7 @@ size="icon" class={cn('h-6 w-6 flex-shrink-0', samplePlayingVoice === v.id ? 'text-(--color-brand) bg-(--color-brand)/15 hover:bg-(--color-brand)/25' : 'text-(--color-muted) hover:text-(--color-text)')} onclick={(e) => { e.stopPropagation(); playSample(v.id); }} - title={samplePlayingVoice === v.id ? 'Stop sample' : 'Play sample'} + title={samplePlayingVoice === v.id ? m.reader_voice_stop_sample() : m.reader_voice_play_sample()} aria-label={samplePlayingVoice === v.id ? `Stop ${v.id} sample` : `Play ${v.id} sample`} > {#if samplePlayingVoice === v.id} @@ -724,7 +725,7 @@ - Audio Narration + {m.reader_audio_narration()} @@ -734,7 +735,7 @@ size="sm" onclick={() => { stopSample(); showVoicePanel = !showVoicePanel; }} class={cn('gap-1.5 text-xs', showVoicePanel ? 'text-(--color-brand) bg-(--color-brand)/15 hover:bg-(--color-brand)/25' : '')} - title="Change voice" + title={m.reader_change_voice()} > @@ -751,13 +752,13 @@ {#if showVoicePanel && voices.length > 0}
- Choose Voice + {m.reader_choose_voice()}

- New voice applies on next "Play narration". + {m.reader_voice_applies_next()} {#if voices.length > 0} {}); }} - >Generate missing samples + >{m.reader_generate_samples()} {/if}

@@ -810,12 +811,12 @@ {#if audioStore.status === 'error'}

{audioStore.errorMsg || 'Failed to load audio.'}

{/if} - + {:else if audioStore.status === 'loading'} {:else if audioStore.status === 'generating'}
-

Generating narration…

+

{m.reader_generating_narration()}

- {#if audioStore.isPlaying} - - - - Playing — controls below - {:else} - - - - Paused — controls below - {/if} + {#if audioStore.isPlaying} + + + + {m.reader_playing()} + {:else} + + + + {m.reader_paused()} + {/if} {formatTime(audioStore.currentTime)} / {formatTime(audioStore.duration)} @@ -864,39 +865,39 @@ variant="ghost" size="sm" class={cn('gap-1.5 text-xs flex-shrink-0', audioStore.autoNext ? 'text-(--color-brand) bg-(--color-brand)/15 hover:bg-(--color-brand)/25' : 'text-(--color-muted)')} - onclick={() => (audioStore.autoNext = !audioStore.autoNext)} - title={audioStore.autoNext ? `Auto-next on — will play Ch.${nextChapter} automatically` : 'Auto-next off'} - aria-pressed={audioStore.autoNext} - > - - - - Auto - + onclick={() => (audioStore.autoNext = !audioStore.autoNext)} + title={audioStore.autoNext ? m.player_auto_next_on() : m.player_auto_next_off()} + aria-pressed={audioStore.autoNext} + > + + + + {m.reader_auto_next()} + {/if}
{#if audioStore.autoNext && nextChapter !== null && nextChapter !== undefined}
- {#if audioStore.nextStatus === 'prefetching'} -
- - - - - Preparing Ch.{nextChapter}… {Math.round(audioStore.nextProgress)}% -
- {:else if audioStore.nextStatus === 'prefetched'} -

- - - - Ch.{nextChapter} ready -

- {:else if audioStore.nextStatus === 'failed'} -

Ch.{nextChapter} will generate on navigate

- {/if} + {#if audioStore.nextStatus === 'prefetching'} +
+ + + + + {m.reader_ch_preparing({ n: String(nextChapter), percent: String(Math.round(audioStore.nextProgress)) })} +
+ {:else if audioStore.nextStatus === 'prefetched'} +

+ + + + {m.reader_ch_ready({ n: String(nextChapter) })} +

+ {:else if audioStore.nextStatus === 'failed'} +

{m.reader_ch_generate_on_nav({ n: String(nextChapter) })}

+ {/if}
{/if} {/if} @@ -905,10 +906,10 @@

- Now playing: {audioStore.chapterTitle || `Ch.${audioStore.chapter}`} + {m.reader_now_playing({ title: audioStore.chapterTitle || `Ch.${audioStore.chapter}` })}

@@ -918,7 +919,7 @@ - Play narration + {m.reader_play_narration()} {/if}
diff --git a/ui/src/lib/components/CommentsSection.svelte b/ui/src/lib/components/CommentsSection.svelte index e0587e0..dfda4bb 100644 --- a/ui/src/lib/components/CommentsSection.svelte +++ b/ui/src/lib/components/CommentsSection.svelte @@ -3,6 +3,7 @@ import { Textarea } from '$lib/components/ui/textarea'; import { cn } from '$lib/utils'; import type { BookComment } from '$lib/types'; + import * as m from '$lib/paraglide/messages.js'; let { slug, isLoggedIn = false, @@ -244,7 +245,7 @@

- Comments + {m.comments_heading()} {#if !loading && totalCount > 0} ({totalCount}) {/if} @@ -258,13 +259,13 @@ size="sm" class={cn('px-2.5 py-1 h-auto text-xs rounded-md', sort === 'top' ? 'bg-(--color-surface-3) text-(--color-text) hover:bg-(--color-surface-3)' : 'text-(--color-muted) hover:text-(--color-text)')} onclick={() => (sort = 'top')} - >Top + >{m.comments_top()} + >{m.comments_new()}

{/if}
@@ -275,7 +276,7 @@