From 7ce44a74a3b052190cfacd4bd5ee6b92cfc620ac Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 6 Jun 2018 16:46:42 +0200 Subject: Add server localization --- client/src/app/core/server/server.service.ts | 76 +- client/src/app/shared/i18n/i18n-utils.ts | 7 + client/src/app/shared/video/video-details.model.ts | 4 +- client/src/app/shared/video/video.model.ts | 8 +- client/src/app/shared/video/video.service.ts | 56 +- client/src/locale/source/server_en_US.xml | 878 +++++++++++++++++++++ client/src/locale/target/player_fr.xml | 379 --------- client/src/locale/target/server_fr.json | 1 + 8 files changed, 981 insertions(+), 428 deletions(-) create mode 100644 client/src/app/shared/i18n/i18n-utils.ts create mode 100644 client/src/locale/source/server_en_US.xml delete mode 100644 client/src/locale/target/player_fr.xml create mode 100644 client/src/locale/target/server_fr.json (limited to 'client') diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index ccae5a151..56d33339e 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts @@ -1,17 +1,20 @@ -import { tap } from 'rxjs/operators' +import { map, share, switchMap, tap } from 'rxjs/operators' import { HttpClient } from '@angular/common/http' -import { Injectable } from '@angular/core' +import { Inject, Injectable, LOCALE_ID } from '@angular/core' import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' -import { ReplaySubject } from 'rxjs' +import { Observable, ReplaySubject } from 'rxjs' import { ServerConfig } from '../../../../../shared' import { About } from '../../../../../shared/models/server/about.model' import { environment } from '../../../environments/environment' import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos' +import { buildFileLocale, getDefaultLocale } from '../../../../../shared/models/i18n' +import { peertubeTranslate } from '@app/shared/i18n/i18n-utils' @Injectable() export class ServerService { private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/' private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' + private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' private static CONFIG_LOCAL_STORAGE_KEY = 'server-config' configLoaded = new ReplaySubject(1) @@ -19,6 +22,7 @@ export class ServerService { videoCategoriesLoaded = new ReplaySubject(1) videoLicencesLoaded = new ReplaySubject(1) videoLanguagesLoaded = new ReplaySubject(1) + localeObservable: Observable private config: ServerConfig = { instance: { @@ -64,8 +68,12 @@ export class ServerService { private videoLanguages: Array> = [] private videoPrivacies: Array> = [] - constructor (private http: HttpClient) { + constructor ( + private http: HttpClient, + @Inject(LOCALE_ID) private localeId: string + ) { this.loadConfigLocally() + this.loadServerLocale() } loadConfig () { @@ -124,26 +132,46 @@ export class ServerService { notifier: ReplaySubject, sort = false ) { - return this.http.get(ServerService.BASE_VIDEO_URL + attributeName) - .subscribe(data => { - Object.keys(data) - .forEach(dataKey => { - hashToPopulate.push({ - id: dataKey, - label: data[dataKey] - }) - }) - - if (sort === true) { - hashToPopulate.sort((a, b) => { - if (a.label < b.label) return -1 - if (a.label === b.label) return 0 - return 1 - }) - } - - notifier.next(true) - }) + this.localeObservable + .pipe( + switchMap(translations => { + return this.http.get(ServerService.BASE_VIDEO_URL + attributeName) + .pipe(map(data => ({ data, translations }))) + }) + ) + .subscribe(({ data, translations }) => { + Object.keys(data) + .forEach(dataKey => { + const label = data[ dataKey ] + + hashToPopulate.push({ + id: dataKey, + label: peertubeTranslate(label, translations) + }) + }) + + if (sort === true) { + hashToPopulate.sort((a, b) => { + if (a.label < b.label) return -1 + if (a.label === b.label) return 0 + return 1 + }) + } + + notifier.next(true) + }) + } + + private loadServerLocale () { + const fileLocale = buildFileLocale(environment.production === true ? this.localeId : 'fr') + + // Default locale, nothing to translate + const defaultFileLocale = buildFileLocale(getDefaultLocale()) + if (fileLocale === defaultFileLocale) return {} + + this.localeObservable = this.http + .get(ServerService.BASE_LOCALE_URL + fileLocale + '/server.json') + .pipe(share()) } private saveConfigLocally (config: ServerConfig) { diff --git a/client/src/app/shared/i18n/i18n-utils.ts b/client/src/app/shared/i18n/i18n-utils.ts new file mode 100644 index 000000000..c1de51b7b --- /dev/null +++ b/client/src/app/shared/i18n/i18n-utils.ts @@ -0,0 +1,7 @@ +function peertubeTranslate (str: string, translations: { [ id: string ]: string }) { + return translations[str] ? translations[str] : str +} + +export { + peertubeTranslate +} diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index 5fc55fca6..19c350ab3 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts @@ -15,8 +15,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { likesPercent: number dislikesPercent: number - constructor (hash: VideoDetailsServerModel) { - super(hash) + constructor (hash: VideoDetailsServerModel, translations = {}) { + super(hash, translations) this.descriptionPath = hash.descriptionPath this.files = hash.files diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index 48d562f9c..d37dc2c3e 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts @@ -5,6 +5,7 @@ import { VideoConstant } from '../../../../../shared/models/videos/video.model' import { getAbsoluteAPIUrl } from '../misc/utils' import { ServerConfig } from '../../../../../shared/models' import { Actor } from '@app/shared/actor/actor.model' +import { peertubeTranslate } from '@app/shared/i18n/i18n-utils' export class Video implements VideoServerModel { by: string @@ -68,7 +69,7 @@ export class Video implements VideoServerModel { minutes.toString() + ':' + secondsPadding + seconds.toString() } - constructor (hash: VideoServerModel) { + constructor (hash: VideoServerModel, translations = {}) { const absoluteAPIUrl = getAbsoluteAPIUrl() this.createdAt = new Date(hash.createdAt.toString()) @@ -98,6 +99,11 @@ export class Video implements VideoServerModel { this.by = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host) this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account) + + this.category.label = peertubeTranslate(this.category.label, translations) + this.licence.label = peertubeTranslate(this.licence.label, translations) + this.language.label = peertubeTranslate(this.language.label, translations) + this.privacy.label = peertubeTranslate(this.privacy.label, translations) } isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index d1e32faeb..c607b7d6a 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts @@ -1,4 +1,4 @@ -import { catchError, map } from 'rxjs/operators' +import { catchError, map, switchMap } from 'rxjs/operators' import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' import { Injectable } from '@angular/core' import { Observable } from 'rxjs' @@ -24,6 +24,7 @@ import { Account } from '@app/shared/account/account.model' import { AccountService } from '@app/shared/account/account.service' import { VideoChannel } from '../../../../../shared/models/videos' import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' +import { ServerService } from '@app/core' @Injectable() export class VideoService { @@ -33,7 +34,8 @@ export class VideoService { constructor ( private authHttp: HttpClient, private restExtractor: RestExtractor, - private restService: RestService + private restService: RestService, + private serverService: ServerService ) {} getVideoViewUrl (uuid: string) { @@ -41,9 +43,13 @@ export class VideoService { } getVideo (uuid: string): Observable { - return this.authHttp.get(VideoService.BASE_VIDEO_URL + uuid) + return this.serverService.localeObservable .pipe( - map(videoHash => new VideoDetails(videoHash)), + switchMap(translations => { + return this.authHttp.get(VideoService.BASE_VIDEO_URL + uuid) + .pipe(map(videoHash => ({ videoHash, translations }))) + }), + map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)), catchError(res => this.restExtractor.handleError(res)) ) } @@ -102,9 +108,10 @@ export class VideoService { let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination, sort) - return this.authHttp.get(UserService.BASE_USERS_URL + '/me/videos', { params }) + return this.authHttp + .get>(UserService.BASE_USERS_URL + '/me/videos', { params }) .pipe( - map(this.extractVideos), + switchMap(res => this.extractVideos(res)), catchError(res => this.restExtractor.handleError(res)) ) } @@ -120,9 +127,9 @@ export class VideoService { params = this.restService.addRestGetParams(params, pagination, sort) return this.authHttp - .get(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params }) + .get>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params }) .pipe( - map(this.extractVideos), + switchMap(res => this.extractVideos(res)), catchError(res => this.restExtractor.handleError(res)) ) } @@ -138,9 +145,9 @@ export class VideoService { params = this.restService.addRestGetParams(params, pagination, sort) return this.authHttp - .get(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.uuid + '/videos', { params }) + .get>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.uuid + '/videos', { params }) .pipe( - map(this.extractVideos), + switchMap(res => this.extractVideos(res)), catchError(res => this.restExtractor.handleError(res)) ) } @@ -160,9 +167,9 @@ export class VideoService { } return this.authHttp - .get(VideoService.BASE_VIDEO_URL, { params }) + .get>(VideoService.BASE_VIDEO_URL, { params }) .pipe( - map(this.extractVideos), + switchMap(res => this.extractVideos(res)), catchError(res => this.restExtractor.handleError(res)) ) } @@ -230,7 +237,7 @@ export class VideoService { return this.authHttp .get>(url, { params }) .pipe( - map(this.extractVideos), + switchMap(res => this.extractVideos(res)), catchError(res => this.restExtractor.handleError(res)) ) } @@ -287,14 +294,19 @@ export class VideoService { } private extractVideos (result: ResultList) { - const videosJson = result.data - const totalVideos = result.total - const videos = [] - - for (const videoJson of videosJson) { - videos.push(new Video(videoJson)) - } - - return { videos, totalVideos } + return this.serverService.localeObservable + .pipe( + map(translations => { + const videosJson = result.data + const totalVideos = result.total + const videos: Video[] = [] + + for (const videoJson of videosJson) { + videos.push(new Video(videoJson, translations)) + } + + return { videos, totalVideos } + }) + ) } } diff --git a/client/src/locale/source/server_en_US.xml b/client/src/locale/source/server_en_US.xml new file mode 100644 index 000000000..dab91f98d --- /dev/null +++ b/client/src/locale/source/server_en_US.xml @@ -0,0 +1,878 @@ + + + + + Music + undefined + + + Films + undefined + + + Vehicles + undefined + + + Art + undefined + + + Sports + undefined + + + Travels + undefined + + + Gaming + undefined + + + People + undefined + + + Comedy + undefined + + + Entertainment + undefined + + + News + undefined + + + How To + undefined + + + Education + undefined + + + Activism + undefined + + + Science & Technology + undefined + + + Animals + undefined + + + Kids + undefined + + + Food + undefined + + + Attribution + undefined + + + Attribution - Share Alike + undefined + + + Attribution - No Derivatives + undefined + + + Attribution - Non Commercial + undefined + + + Attribution - Non Commercial - Share Alike + undefined + + + Attribution - Non Commercial - No Derivatives + undefined + + + Public Domain Dedication + undefined + + + Public + undefined + + + Unlisted + undefined + + + Private + undefined + + + Afar + undefined + + + Abkhazian + undefined + + + Afrikaans + undefined + + + Akan + undefined + + + Amharic + undefined + + + Arabic + undefined + + + Aragonese + undefined + + + American Sign Language + undefined + + + Assamese + undefined + + + Avaric + undefined + + + Aymara + undefined + + + Azerbaijani + undefined + + + Bashkir + undefined + + + Bambara + undefined + + + Belarusian + undefined + + + Bengali + undefined + + + British Sign Language + undefined + + + Bislama + undefined + + + Tibetan + undefined + + + Bosnian + undefined + + + Breton + undefined + + + Bulgarian + undefined + + + Brazilian Sign Language + undefined + + + Catalan + undefined + + + Czech + undefined + + + Chamorro + undefined + + + Chechen + undefined + + + Chuvash + undefined + + + Cornish + undefined + + + Corsican + undefined + + + Cree + undefined + + + Czech Sign Language + undefined + + + Chinese Sign Language + undefined + + + Welsh + undefined + + + Danish + undefined + + + German + undefined + + + Dhivehi + undefined + + + Danish Sign Language + undefined + + + Dzongkha + undefined + + + Modern Greek (1453-) + undefined + + + English + undefined + + + Estonian + undefined + + + Basque + undefined + + + Ewe + undefined + + + Faroese + undefined + + + Persian + undefined + + + Fijian + undefined + + + Finnish + undefined + + + French + undefined + + + Western Frisian + undefined + + + French Sign Language + undefined + + + Fulah + undefined + + + Scottish Gaelic + undefined + + + Irish + undefined + + + Galician + undefined + + + Manx + undefined + + + Guarani + undefined + + + German Sign Language + undefined + + + Gujarati + undefined + + + Haitian + undefined + + + Hausa + undefined + + + Serbo-Croatian + undefined + + + Hebrew + undefined + + + Herero + undefined + + + Hindi + undefined + + + Hiri Motu + undefined + + + Croatian + undefined + + + Hungarian + undefined + + + Armenian + undefined + + + Igbo + undefined + + + Sichuan Yi + undefined + + + Inuktitut + undefined + + + Indonesian + undefined + + + Inupiaq + undefined + + + Icelandic + undefined + + + Italian + undefined + + + Javanese + undefined + + + Japanese + undefined + + + Japanese Sign Language + undefined + + + Kalaallisut + undefined + + + Kannada + undefined + + + Kashmiri + undefined + + + Georgian + undefined + + + Kanuri + undefined + + + Kazakh + undefined + + + Khmer + undefined + + + Kikuyu + undefined + + + Kinyarwanda + undefined + + + Kirghiz + undefined + + + Komi + undefined + + + Kongo + undefined + + + Korean + undefined + + + Kuanyama + undefined + + + Kurdish + undefined + + + Lao + undefined + + + Latvian + undefined + + + Limburgan + undefined + + + Lingala + undefined + + + Lithuanian + undefined + + + Luxembourgish + undefined + + + Luba-Katanga + undefined + + + Ganda + undefined + + + Marshallese + undefined + + + Malayalam + undefined + + + Marathi + undefined + + + Macedonian + undefined + + + Malagasy + undefined + + + Maltese + undefined + + + Mongolian + undefined + + + Maori + undefined + + + Malay (macrolanguage) + undefined + + + Burmese + undefined + + + Nauru + undefined + + + Navajo + undefined + + + South Ndebele + undefined + + + North Ndebele + undefined + + + Ndonga + undefined + + + Nepali (macrolanguage) + undefined + + + Dutch + undefined + + + Norwegian Nynorsk + undefined + + + Norwegian Bokmål + undefined + + + Norwegian + undefined + + + Nyanja + undefined + + + Occitan (post 1500) + undefined + + + Ojibwa + undefined + + + Oriya (macrolanguage) + undefined + + + Oromo + undefined + + + Ossetian + undefined + + + Panjabi + undefined + + + Pakistan Sign Language + undefined + + + Polish + undefined + + + Portuguese + undefined + + + Pushto + undefined + + + Quechua + undefined + + + Romansh + undefined + + + Romanian + undefined + + + Russian Sign Language + undefined + + + Rundi + undefined + + + Russian + undefined + + + Sango + undefined + + + Saudi Arabian Sign Language + undefined + + + South African Sign Language + undefined + + + Sinhala + undefined + + + Slovak + undefined + + + Slovenian + undefined + + + Northern Sami + undefined + + + Samoan + undefined + + + Shona + undefined + + + Sindhi + undefined + + + Somali + undefined + + + Southern Sotho + undefined + + + Spanish + undefined + + + Albanian + undefined + + + Sardinian + undefined + + + Serbian + undefined + + + Swati + undefined + + + Sundanese + undefined + + + Swahili (macrolanguage) + undefined + + + Swedish + undefined + + + Swedish Sign Language + undefined + + + Tahitian + undefined + + + Tamil + undefined + + + Tatar + undefined + + + Telugu + undefined + + + Tajik + undefined + + + Tagalog + undefined + + + Thai + undefined + + + Tigrinya + undefined + + + Tonga (Tonga Islands) + undefined + + + Tswana + undefined + + + Tsonga + undefined + + + Turkmen + undefined + + + Turkish + undefined + + + Twi + undefined + + + Uighur + undefined + + + Ukrainian + undefined + + + Urdu + undefined + + + Uzbek + undefined + + + Venda + undefined + + + Vietnamese + undefined + + + Walloon + undefined + + + Wolof + undefined + + + Xhosa + undefined + + + Yiddish + undefined + + + Yoruba + undefined + + + Zhuang + undefined + + + Chinese + undefined + + + Zulu + undefined + + + Misc + undefined + + + Unknown + undefined + + + + \ No newline at end of file diff --git a/client/src/locale/target/player_fr.xml b/client/src/locale/target/player_fr.xml deleted file mode 100644 index eafa4baff..000000000 --- a/client/src/locale/target/player_fr.xml +++ /dev/null @@ -1,379 +0,0 @@ - - - - - - - Audio Player - Lecteur audio - - - Video Player - Lecteur vidéo - - - Play - Lecture - - - Pause - Pause - - - Replay - Revoir - - - Current Time - Temps actuel - - - Duration - Durée - - - Remaining Time - Temps restant - - - Stream Type - Type de flux - - - LIVE - EN DIRECT - - - Loaded - Chargé - - - Progress - Progression - - - Progress Bar - Barre de progression - - - {1} of {2} - {1} de {2} - - - Fullscreen - Plein écran - - - Non-Fullscreen - Fenêtré - - - Mute - Sourdine - - - Unmute - Son activé - - - Playback Rate - Vitesse de lecture - - - Subtitles - Sous-titres - - - subtitles off - Sous-titres désactivés - - - Captions - Sous-titres transcrits - - - captions off - Sous-titres transcrits désactivés - - - Chapters - Chapitres - - - Descriptions - Descriptions - - - descriptions off - descriptions désactivées - - - Audio Track - Piste audio - - - Volume Level - Niveau de volume - - - You aborted the media playback - Vous avez interrompu la lecture de la vidéo. - - - A network error caused the media download to fail part-way. - Une erreur de réseau a interrompu le téléchargement de la vidéo. - - - The media could not be loaded, either because the server or network failed or because the format is not supported. - Cette vidéo n'a pas pu être chargée, soit parce que le serveur ou le réseau a échoué ou parce que le format n'est pas reconnu. - - - The media playback was aborted due to a corruption problem or because the media used features your browser did not support. - La lecture de la vidéo a été interrompue à cause d'un problème de corruption ou parce que la vidéo utilise des fonctionnalités non prises en charge par votre navigateur. - - - No compatible source was found for this media. - Aucune source compatible n'a été trouvée pour cette vidéo. - - - The media is encrypted and we do not have the keys to decrypt it. - Le média est chiffré et nous n'avons pas les clés pour le déchiffrer. - - - Play Video - Lire la vidéo - - - Close - Fermer - - - Close Modal Dialog - Fermer la boîte de dialogue modale - - - Modal Window - Fenêtre modale - - - This is a modal window - Ceci est une fenêtre modale - - - This modal can be closed by pressing the Escape key or activating the close button. - Ce modal peut être fermé en appuyant sur la touche Échap ou activer le bouton de fermeture. - - - , opens captions settings dialog - , ouvrir les paramètres des sous-titres transcrits - - - , opens subtitles settings dialog - , ouvrir les paramètres des sous-titres - - - , opens descriptions settings dialog - , ouvrir les paramètres des descriptions - - - , selected - , sélectionné - - - captions settings - Paramètres des sous-titres transcrits - - - subititles settings - Paramètres des sous-titres - - - descriptions settings - Paramètres des descriptions - - - Text - Texte - - - White - Blanc - - - Black - Noir - - - Red - Rouge - - - Green - Vert - - - Blue - Bleu - - - Yellow - Jaune - - - Magenta - Magenta - - - Cyan - Cyan - - - Background - Arrière-plan - - - Window - Fenêtre - - - Transparent - Transparent - - - Semi-Transparent - Semi-transparent - - - Opaque - Opaque - - - Font Size - Taille des caractères - - - Text Edge Style - Style des contours du texte - - - None - Aucun - - - Raised - Élevé - - - Depressed - Enfoncé - - - Uniform - Uniforme - - - Dropshadow - Ombre portée - - - Font Family - Familles de polices - - - Proportional Sans-Serif - Polices à chasse variable sans empattement (Proportional Sans-Serif) - - - Monospace Sans-Serif - Polices à chasse fixe sans empattement (Monospace Sans-Serif) - - - Proportional Serif - Polices à chasse variable avec empattement (Proportional Serif) - - - Monospace Serif - Polices à chasse fixe avec empattement (Monospace Serif) - - - Casual - Manuscrite - - - Script - Scripte - - - Small Caps - Petites capitales - - - Reset - Réinitialiser - - - restore all settings to the default values - Restaurer tous les paramètres aux valeurs par défaut - - - Done - Terminé - - - Caption Settings Dialog - Boîte de dialogue des paramètres des sous-titres transcrits - - - Beginning of dialog window. Escape will cancel and close the window. - Début de la fenêtre de dialogue. La touche d'échappement annulera et fermera la fenêtre. - - - End of dialog window. - Fin de la fenêtre de dialogue. - - - {1} is loading. - {1} est en train de charger - - - Quality - Qualité - - - Auto - Auto - - - Speed - Vitesse - - - peers - pairs - - - Go to the video page - Aller sur la page de la vidéo - - - Settings - Paramètres - - - Uses P2P, others may know you are watching this video. - Utilise le P2P, d'autres personnes pourraient savoir que vous regardez cette vidéo. - - - Copy the video URL - Copier le lien de la vidéo - - - Copy the video URL at the current time - Copier le lien de la vidéo à partir de cette séquence - - - Copy embed code - Copier le code d'intégration - - - \ No newline at end of file diff --git a/client/src/locale/target/server_fr.json b/client/src/locale/target/server_fr.json new file mode 100644 index 000000000..43216adf4 --- /dev/null +++ b/client/src/locale/target/server_fr.json @@ -0,0 +1 @@ +{"Music":"Musique","Films":"Films","Vehicles":"Transport","Art":"Art","Sports":"Sports","Travels":"Voyages","Gaming":"Jeux vidéos","People":"People","Comedy":"Humour","Entertainment":"Divertissement","News":"Actualités","How To":"Tutoriel","Education":"Éducation","Activism":"Activisme","Science & Technology":"Science & Technologie","Animals":"Animaux","Kids":"Enfants","Food":"Cuisine","Attribution":"Attribution","Attribution - Share Alike":"Attribution - Partage dans les mêmes conditions","Attribution - No Derivatives":"Attribution - Pas d'oeuvre dérivée","Attribution - Non Commercial":"Attribution - Utilisation non commerciale","Attribution - Non Commercial - Share Alike":"Attribution - Utilisation non commerciale - Partage dans les mêmes conditions","Attribution - Non Commercial - No Derivatives":"Attribution - Utilisation non commerciale - Pas d'oeuvre dérivée","Public Domain Dedication":"Domaine public","Public":"Publique","Unlisted":"Non listée","Private":"Privée","French":"Français","French Sign Language":"Langage des signes français","Misc":"Divers","Unknown":"Inconnu"} \ No newline at end of file -- cgit v1.2.3