Files
NewTube/src/services/i18n.service.ts
T
bruno 66f1da1d72 feat(mobile): refonte responsive complète (header, drawer, dvh, cibles tactiles)
Le header était en grid-cols-[auto_1fr_auto] avec une barre de recherche
non-wrappable (7 chips + input + 2 boutons, aucun min-w-0) : ~1000px de
largeur intrinsèque dans un viewport de 375px. Toutes les routes
débordaient horizontalement et les boutons Connexion/Inscription partaient
hors écran.

Socle
- --header-h / --header-offset / --app-vh centralisent la métrique du
  header : supprime les top-16 codés en dur (channel-tabs était en top-0 et
  glissait sous le header au scroll).
- Safe-area iOS, viewport-fit=cover, color-scheme/theme-color.
- dvh partout (100vh > hauteur visible sur iOS), overflow-x: clip au lieu de
  hidden (hidden cassait silencieusement tous les sticky).
- Cible tactile 44px sur pointeur grossier, skip link, focus-ring clavier,
  scroll-rail pour les tab strips.

Header
- Barre mobile (burger, logo, recherche, menu ⋮) + barre desktop inchangée.
- Le menu ⋮ expose ce qui disparaissait : bande des thèmes, préférences,
  historique, favoris, téléchargements, thème, connexion/inscription.
- Recherche en feuille sous le header avec focus automatique.
- Échap / clic extérieur / navigation ferment les panneaux transitoires.

Drawer
- Partait à inset-y-0 : les 56 premiers px du menu étaient derrière le
  header. Descend sous le header, bouton fermer intégré, verrouille le
  scroll, se ferme à Échap.

Barre de recherche
- Chips masquées sous lg, remplacées par un bouton résumé qui ouvre le
  sélecteur de fournisseurs. Suggestions en dvh, menu Ctrl+K plein écran
  sur mobile.

Pages
- Shorts : cadre 9:16 contraint par dvh ET vw (il débordait en paysage),
  Préc./Suiv. qui étaient hors du conteneur height-capped.
- Watch : barre d'actions qui wrappe, mode cinéma en opacity-0 group-hover
  donc invisible au doigt, thumbnails fluides, playsinline sur les iframes.
- Historique, thèmes, chaîne, playlists, préférences, sessions : rails
  swipables, min-w-0, truncate, en-têtes qui wrappent.

Vérifié : audit navigateur (CDP) sur 17 routes x 7 largeurs (320→1440),
zéro débordement horizontal ; 19 tests d'interaction mobile ; suites de
tests du dépôt vertes (api 40/40, transcript 27/27, search, suggest,
playlists, preferences, subscriptions, highlight, telemetry).
2026-09-28 14:38:31 -04:00

360 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Injectable, computed, signal } from '@angular/core';
export type LangCode = 'en' | 'fr';
// Simple key-value dictionaries. Extend as needed.
const EN: Record<string, string> = {
'app.name': 'NewTube',
// Header
'search.placeholder': 'Search...',
'label.provider': 'Provider',
'label.region': 'Region',
// Regions
'region.US': 'United States',
'region.FR': 'France',
'region.BR': 'Brazil',
'region.DE': 'Germany',
'region.GB': 'United Kingdom',
'region.CA': 'Canada',
'btn.edit': 'Edit',
'btn.login': 'Login',
'btn.register': 'Register',
'menu.account': 'Account',
'menu.preferences': 'Preferences',
'menu.appearance': 'Appearance',
'menu.logout': 'Logout',
'menu.more': 'More options',
'menu.search': 'Search',
'menu.close': 'Close',
'menu.navigation': 'Navigation',
'menu.theme': 'Theme',
'theme.system': 'System',
'theme.black': 'Black',
'theme.dark': 'Dark',
'theme.blue': 'Blue',
'theme.light': 'Light',
// Preferences
'preferences.title': 'Preferences',
'preferences.general': 'General',
'preferences.general_hint': 'Customize your experience',
'preferences.peertube_hint': 'Manage your PeerTube instances',
'preferences.add_instance_placeholder': 'e.g. example.org',
'preferences.no_instances': 'No instances yet.',
'preferences.active': 'Active',
'preferences.set_active': 'Set Active',
'label.language': 'Language',
'label.defaultProvider': 'Default Provider',
'label.theme': 'Theme',
'label.quality': 'Default Quality',
'preferences.download_languages': 'Languages to download',
'preferences.download_languages_hint': 'Only enabled languages are displayed and downloaded (subtitles/transcripts). French and English by default.',
'preferences.download_languages_empty': 'Select at least one language.',
'btn.save': 'Save',
'btn.add': 'Add',
'btn.remove': 'Remove',
// Sidebar
'nav.home': 'Home',
'nav.shorts': 'Shorts',
'nav.subscriptions': 'Subscriptions',
'nav.groups': 'Groups',
'nav.you': 'You',
'nav.history': 'History',
'nav.playlists': 'Playlists',
'nav.liked': 'Liked videos',
'nav.downloads': 'Downloads',
'nav.import': 'Import',
'nav.explore': 'Explore',
'nav.providers': 'Providers',
'nav.info': 'Information',
'nav.usage': 'Usage',
'explore.music': 'Music',
'explore.gaming': 'Gaming',
'explore.news': 'News',
'explore.sports': 'Sports',
'explore.learning': 'Learning',
// Home
'home.trending': 'Trending',
'loading.more': 'Loading more...',
'search.hint': 'Type something in the search bar to get started.',
'search.noResults': 'No results found.',
// Themes + Provider/Theme pages
'themes.viewAll': 'View All',
'themes.live': 'Live Streams',
'themes.recorded': 'Recorded Streams',
'themes.videos': 'Videos',
'themes.categories': 'Categories',
'themes.backToThemes': 'Back to themes',
'themes.changeProvider': 'Change provider',
'themes.title': 'Themes',
'themes.show': 'Show',
'themes.hide': 'Hide',
'themes.showBar': 'Show the themes bar',
'themes.hideBar': 'Hide the themes bar',
// Common
'common.error': 'An error occurred.',
'empty.noItems': 'No items.',
'common.more': 'More',
'common.less': 'Less',
'common.skipToContent': 'Skip to main content',
'common.closeMenu': 'Close menu',
// Filters
'filter.sort': 'Sort by',
'filter.sort.recent': 'Most recent',
'filter.sort.viewed': 'Most viewed',
'filter.sort.longest': 'Longest',
'filter.sort.relevance': 'Relevance',
'filter.sort.date': 'Date',
'filter.sort.views': 'Views',
'filter.duration': 'Duration',
'filter.duration.any': 'Any',
'filter.duration.short': 'Short (<10m)',
'filter.duration.medium': 'Medium (10–30m)',
'filter.duration.long': 'Long (30m+)',
'filter.type': 'Type',
'filter.type.any': 'Any',
'filter.type.video': 'Video',
'filter.type.live': 'Live',
'filter.type.recorded': 'Recorded',
'filter.type.short': 'Shorts',
'filter.type.channel': 'Channel',
'filter.language': 'Language',
'filter.language.any': 'Any',
'filter.language.en': 'English',
'filter.language.fr': 'French',
'filter.date': 'Date',
'filter.date.any': 'Any',
'filter.date.day': 'Last 24h',
'filter.date.week': 'Last week',
'filter.date.month': 'Last month',
'filter.date.year': 'Last year',
// Optional per-theme labels
'theme.finance-crypto': 'Finance & Crypto',
'theme.gaming': 'Gaming',
'theme.news': 'News',
'theme.sports': 'Sports',
// Downloads
'downloads.title': 'Downloads',
'downloads.refresh': 'Refresh',
'downloads.quota': 'Storage used',
'downloads.filter.all': 'All',
'downloads.filter.active': 'Active',
'downloads.filter.completed': 'Completed',
'downloads.filter.failed': 'Failed',
'downloads.empty': 'No downloads yet. Start one from a video page.',
'downloads.audioOnly': 'Audio only',
'downloads.getFile': 'Get file',
'downloads.play': 'Play',
'downloads.fileError': 'Unable to fetch the file. Please sign in again and retry.',
'downloads.back': 'Back to downloads',
'downloads.notReady': 'This download is not completed yet.',
'downloads.loadError': 'Unable to load this download.',
'downloads.retry': 'Retry',
'downloads.state.queued': 'Queued',
'downloads.state.running': 'Downloading',
'downloads.state.merging': 'Merging',
'downloads.state.completed': 'Completed',
'downloads.state.failed': 'Failed',
'downloads.state.interrupted': 'Interrupted',
};
const FR: Record<string, string> = {
'app.name': 'NewTube',
// Header
'search.placeholder': 'Rechercher...',
'label.provider': 'Fournisseur',
'label.region': 'Région',
// Régions
'region.US': 'États-Unis',
'region.FR': 'France',
'region.BR': 'Brésil',
'region.DE': 'Allemagne',
'region.GB': 'Royaume-Uni',
'region.CA': 'Canada',
'btn.edit': 'Modifier',
'btn.login': 'Connexion',
'btn.register': 'Inscription',
'menu.account': 'Compte',
'menu.preferences': 'Préférences',
'menu.appearance': 'Apparence',
'menu.logout': 'Déconnexion',
'menu.more': 'Plus d\'options',
'menu.search': 'Rechercher',
'menu.close': 'Fermer',
'menu.navigation': 'Navigation',
'menu.theme': 'Thème',
'theme.system': 'Système',
'theme.black': 'Noir',
'theme.dark': 'Sombre',
'theme.blue': 'Bleu',
'theme.light': 'Clair',
// Preferences
'preferences.title': 'Préférences',
'preferences.general': 'Préférences générales',
'preferences.general_hint': 'Personnalisez votre expérience',
'preferences.peertube_hint': 'Gérez vos instances PeerTube',
'preferences.add_instance_placeholder': 'ex: example.org',
'preferences.no_instances': 'Aucune instance pour le moment.',
'preferences.active': 'Actif',
'preferences.set_active': 'Définir actif',
'label.language': 'Langue',
'label.defaultProvider': 'Fournisseur par défaut',
'label.theme': 'Thème',
'label.quality': 'Qualité par défaut',
'preferences.download_languages': 'Langues à télécharger',
'preferences.download_languages_hint': 'Seules les langues activées sont affichées et téléchargées (sous-titres/transcriptions). Français et anglais par défaut.',
'preferences.download_languages_empty': 'Sélectionnez au moins une langue.',
'btn.save': 'Enregistrer',
'btn.add': 'Ajouter',
'btn.remove': 'Supprimer',
// Sidebar
'nav.home': 'Accueil',
'nav.shorts': 'Shorts',
'nav.subscriptions': 'Abonnements',
'nav.groups': 'Groupes',
'nav.you': 'Vous',
'nav.history': 'Historique',
'nav.playlists': 'Listes de lecture',
'nav.liked': 'Vidéos que vous aimez',
'nav.downloads': 'Téléchargements',
'nav.import': 'Importer',
'nav.explore': 'Explorer',
'nav.providers': 'Fournisseurs',
'nav.info': 'Informations',
'nav.usage': 'Utilisation',
'explore.music': 'Musique',
'explore.gaming': 'Jeux vidéo',
'explore.news': 'Actualités',
'explore.sports': 'Sports',
'explore.learning': 'Apprentissage',
// Home
'home.trending': 'Tendances',
'loading.more': 'Chargement...',
'search.hint': 'Tapez quelque chose dans la barre de recherche pour commencer.',
'search.noResults': 'Aucun résultat.',
// Themes + Provider/Theme pages
'themes.viewAll': 'Voir tout',
'themes.live': 'En direct',
'themes.recorded': 'Rediffusions',
'themes.videos': 'Vidéos',
'themes.categories': 'Catégories',
'themes.backToThemes': 'Retour aux thèmes',
'themes.changeProvider': 'Changer de provider',
'themes.title': 'Thèmes',
'themes.show': 'Afficher',
'themes.hide': 'Masquer',
'themes.showBar': 'Afficher la bande des thèmes',
'themes.hideBar': 'Masquer la bande des thèmes',
// Common
'common.error': 'Une erreur est survenue.',
'empty.noItems': 'Aucun élément.',
'common.more': 'Plus',
'common.less': 'Moins',
'common.skipToContent': 'Aller au contenu principal',
'common.closeMenu': 'Fermer le menu',
// Filters
'filter.sort': 'Tri',
'filter.sort.recent': 'Plus récents',
'filter.sort.viewed': 'Plus vus',
'filter.sort.longest': 'Plus longs',
'filter.sort.relevance': 'Pertinence',
'filter.sort.date': 'Date',
'filter.sort.views': 'Vues',
'filter.duration': 'Durée',
'filter.duration.any': 'Toute',
'filter.duration.short': 'Courte (<10m)',
'filter.duration.medium': 'Moyenne (10–30m)',
'filter.duration.long': 'Longue (30m+)',
'filter.type': 'Type',
'filter.type.any': 'Tous',
'filter.type.video': 'Vidéo',
'filter.type.live': 'Direct',
'filter.type.recorded': 'Rediffusion',
'filter.type.short': 'Shorts',
'filter.type.channel': 'Chaîne',
'filter.language': 'Langue',
'filter.language.any': 'Toutes',
'filter.language.en': 'Anglais',
'filter.language.fr': 'Français',
'filter.date': 'Date',
'filter.date.any': 'Toutes',
'filter.date.day': '24 dernières h',
'filter.date.week': 'Semaine dernière',
'filter.date.month': 'Mois dernier',
'filter.date.year': 'Année dernière',
// Optional per-theme labels
'theme.finance-crypto': 'Finance & Crypto',
'theme.gaming': 'Jeux vidéo',
'theme.news': 'Actualités',
'theme.sports': 'Sports',
// Downloads
'downloads.title': 'Téléchargements',
'downloads.refresh': 'Rafraîchir',
'downloads.quota': 'Stockage utilisé',
'downloads.filter.all': 'Tous',
'downloads.filter.active': 'Actifs',
'downloads.filter.completed': 'Terminés',
'downloads.filter.failed': 'Échoués',
'downloads.empty': 'Aucun téléchargement. Lancez-en un depuis une page vidéo.',
'downloads.audioOnly': 'Audio seul',
'downloads.getFile': 'Obtenir le fichier',
'downloads.play': 'Lire',
'downloads.fileError': 'Fichier inaccessible. Reconnectez-vous puis réessayez.',
'downloads.back': 'Retour aux téléchargements',
'downloads.notReady': 'Ce téléchargement n’est pas encore terminé.',
'downloads.loadError': 'Impossible de charger ce téléchargement.',
'downloads.retry': 'Réessayer',
'downloads.state.queued': 'En attente',
'downloads.state.running': 'Téléchargement',
'downloads.state.merging': 'Fusion',
'downloads.state.completed': 'Terminé',
'downloads.state.failed': 'Échoué',
'downloads.state.interrupted': 'Interrompu',
};
@Injectable({ providedIn: 'root' })
export class I18nService {
private readonly LS_KEY = 'newtube.lang';
private readonly dictionaries: Record<string, Record<string, string>> = { en: EN, fr: FR };
private readonly defaultLang: LangCode = 'en';
// initialize from localStorage, fallback 'en'
lang = signal<LangCode>((() => {
try {
const ls = (localStorage.getItem(this.LS_KEY) || '').toLowerCase();
return (ls === 'fr' ? 'fr' : 'en') as LangCode;
} catch {
return 'en';
}
})());
readonly currentDict = computed(() => this.dictionaries[this.lang()] || this.dictionaries[this.defaultLang]);
constructor() {
try {
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('lang', this.lang());
}
} catch {}
}
setLanguage(l: string | null | undefined) {
const v = (String(l || '').toLowerCase() === 'fr') ? 'fr' : 'en';
this.lang.set(v as LangCode);
try {
localStorage.setItem(this.LS_KEY, v);
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('lang', v);
}
} catch {}
}
t(key: string, params?: Record<string, any>): string {
const dict = this.currentDict();
const base = dict[key] ?? this.dictionaries[this.defaultLang][key] ?? key;
if (params) {
return Object.keys(params).reduce((acc, k) => acc.replace(new RegExp('{' + k + '}', 'g'), String(params[k])), base);
}
return base;
}
}