mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 01:39:37 +02:00
Add email translations
Convert emails from Pug template to Handlebars because i18next doesn't support Pug
This commit is contained in:
parent
b45fbf4337
commit
d6e4dac032
223 changed files with 9859 additions and 1426 deletions
15
.github/CONTRIBUTING.md
vendored
15
.github/CONTRIBUTING.md
vendored
|
@ -223,8 +223,19 @@ Instance configurations are in `config/test-{1,2,3}.yaml`.
|
|||
|
||||
To test emails with PeerTube:
|
||||
|
||||
* Run [mailslurper](http://mailslurper.com/)
|
||||
* Run PeerTube using mailslurper SMTP port: `NODE_CONFIG='{ "smtp": { "hostname": "localhost", "port": 2500, "tls": false } }' NODE_ENV=dev node dist/server`
|
||||
* Run [MailDev](https://github.com/maildev/maildev) using Docker
|
||||
* Run PeerTube using MailDev SMTP port: `NODE_CONFIG='{ "smtp": { "hostname": "localhost", "port": 2500, "tls": false } }' NODE_ENV=dev node dist/server`
|
||||
|
||||
To test all emails without having to run actions manually on the web interface, you can run notification unit tests with environment variables to relay emails to your MailDev instance. For example:
|
||||
|
||||
```sh
|
||||
MAILDEV_RELAY_HOST=localhost MAILDEV_RELAY_PORT=2500 mocha --exit --bail packages/tests/src/api/notifications/comments-notifications.ts
|
||||
```
|
||||
|
||||
You can then go to the MailDev web interface and see how emails look like.
|
||||
|
||||
The admin web interface also have a button to send some email templates to a specific email address.
|
||||
|
||||
|
||||
### Environment variables
|
||||
|
||||
|
|
|
@ -73,6 +73,18 @@
|
|||
></my-markdown-textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceDefaultLanguage">Default language</label>
|
||||
<div class="form-group-description">
|
||||
<div i18n>Default language used for users, in emails for example.</div>
|
||||
<div i18n>The web interface still uses the web browser preferred language if not overridden by user preference</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<my-select-options inputId="instanceDefaultLanguage" formControlName="defaultLanguage" [items]="defaultLanguageItems"></my-select-options>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceCategories">Main instance categories</label>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router'
|
||||
import { CanComponentDeactivate, ServerService } from '@app/core'
|
||||
|
@ -17,8 +17,11 @@ import {
|
|||
} from '@app/shared/form-validators/form-validator.model'
|
||||
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
||||
import { SelectRadioComponent } from '@app/shared/shared-forms/select/select-radio.component'
|
||||
import { getCompleteLocale, I18N_LOCALES } from '@peertube/peertube-core-utils'
|
||||
import { ActorImage, CustomConfig, NSFWPolicyType, VideoConstant } from '@peertube/peertube-models'
|
||||
import merge from 'lodash-es/merge'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
import { AdminConfigService } from '../../../shared/shared-admin/admin-config.service'
|
||||
|
@ -44,6 +47,7 @@ type Form = {
|
|||
shortDescription: FormControl<string>
|
||||
description: FormControl<string>
|
||||
categories: FormControl<number[]>
|
||||
defaultLanguage: FormControl<string>
|
||||
languages: FormControl<string[]>
|
||||
serverCountry: FormControl<string>
|
||||
|
||||
|
@ -87,7 +91,8 @@ type Form = {
|
|||
PeertubeCheckboxComponent,
|
||||
PeerTubeTemplateDirective,
|
||||
HelpComponent,
|
||||
AdminSaveBarComponent
|
||||
AdminSaveBarComponent,
|
||||
SelectOptionsComponent
|
||||
]
|
||||
})
|
||||
export class AdminConfigInformationComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
|
@ -126,6 +131,8 @@ export class AdminConfigInformationComponent implements OnInit, OnDestroy, CanCo
|
|||
}
|
||||
]
|
||||
|
||||
defaultLanguageItems: SelectOptionsItem[] = []
|
||||
|
||||
private customConfig: CustomConfig
|
||||
private customConfigSub: Subscription
|
||||
|
||||
|
@ -143,6 +150,7 @@ export class AdminConfigInformationComponent implements OnInit, OnDestroy, CanCo
|
|||
|
||||
this.languageItems = data.languages.map(l => ({ label: l.label, id: l.id }))
|
||||
this.categoryItems = data.categories.map(l => ({ label: l.label, id: l.id }))
|
||||
this.defaultLanguageItems = Object.entries(I18N_LOCALES).map(([ id, label ]) => ({ label, id }))
|
||||
|
||||
this.buildForm()
|
||||
|
||||
|
@ -185,6 +193,8 @@ export class AdminConfigInformationComponent implements OnInit, OnDestroy, CanCo
|
|||
|
||||
hardwareInformation: null,
|
||||
|
||||
defaultLanguage: null,
|
||||
|
||||
categories: null,
|
||||
languages: null,
|
||||
|
||||
|
@ -200,7 +210,14 @@ export class AdminConfigInformationComponent implements OnInit, OnDestroy, CanCo
|
|||
}
|
||||
}
|
||||
|
||||
const defaultValues: FormDefaultTyped<Form> = this.customConfig
|
||||
const defaultValues: FormDefaultTyped<Form> = merge(
|
||||
this.customConfig,
|
||||
{
|
||||
instance: {
|
||||
defaultLanguage: getCompleteLocale(this.customConfig.instance.defaultLanguage)
|
||||
}
|
||||
} satisfies FormDefaultTyped<Form>
|
||||
)
|
||||
|
||||
const {
|
||||
form,
|
||||
|
|
|
@ -1,15 +1,33 @@
|
|||
<h2 class="fs-5" i18n>IP address</h2>
|
||||
<div class="root">
|
||||
<div>
|
||||
<h2 class="fs-5" i18n>IP address</h2>
|
||||
|
||||
<p i18n>PeerTube thinks your web browser public IP is <strong>{{ debug?.ip }}</strong>.</p>
|
||||
<p i18n>PeerTube thinks your web browser public IP is <strong>{{ debug?.ip }}</strong>.</p>
|
||||
|
||||
<p i18n>If this is not your correct public IP, please consider fixing it because:</p>
|
||||
<ul>
|
||||
<li i18n>Views may not be counted correctly (reduced compared to what they should be)</li>
|
||||
<li i18n>Anti brute force system could be overzealous</li>
|
||||
<li i18n>P2P system could not work correctly</li>
|
||||
</ul>
|
||||
<p i18n>If this is not your correct public IP, please consider fixing it because:</p>
|
||||
<ul>
|
||||
<li i18n>Views may not be counted correctly (reduced compared to what they should be)</li>
|
||||
<li i18n>Anti brute force system could be overzealous</li>
|
||||
<li i18n>P2P system could not work correctly</li>
|
||||
</ul>
|
||||
|
||||
<p i18n>To fix it:<p>
|
||||
<ul>
|
||||
<li i18n>Check the <code>trust_proxy</code> configuration key</li>
|
||||
</ul>
|
||||
<p i18n>To fix it:<p>
|
||||
<ul>
|
||||
<li i18n>Check the <code>trust_proxy</code> configuration key</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-4" [hidden]="isEmailDisabled()">
|
||||
<h2 class="fs-5" i18n>Emails</h2>
|
||||
|
||||
<form (ngSubmit)="sendTestEmails()">
|
||||
<div class="form-group">
|
||||
<label i18n for="fromName">Send test emails to this address</label>
|
||||
<div i18n class="form-group-description">PeerTube can send all available email templates to the following email address</div>
|
||||
<input [(ngModel)]="testEmail" type="email" id="test-email" name="test-email" class="form-control">
|
||||
</div>
|
||||
|
||||
<input type="submit" i18n-value value="Send test emails" class="peertube-button primary-button" [disabled]="!testEmail" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
|
||||
code {
|
||||
font-size: 14px;
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
|
||||
.root {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
|
|
@ -1,23 +1,31 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { Notifier } from '@app/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { Debug } from '@peertube/peertube-models'
|
||||
import { DebugService } from './debug.service'
|
||||
|
||||
@Component({
|
||||
templateUrl: './debug.component.html',
|
||||
styleUrls: [ './debug.component.scss' ],
|
||||
imports: []
|
||||
imports: [ CommonModule, FormsModule ]
|
||||
})
|
||||
export class DebugComponent implements OnInit {
|
||||
private debugService = inject(DebugService)
|
||||
private notifier = inject(Notifier)
|
||||
private server = inject(ServerService)
|
||||
|
||||
debug: Debug
|
||||
testEmail: string
|
||||
|
||||
ngOnInit (): void {
|
||||
this.load()
|
||||
}
|
||||
|
||||
isEmailDisabled () {
|
||||
return this.server.getHTMLConfig().email.enabled === false
|
||||
}
|
||||
|
||||
load () {
|
||||
this.debugService.getDebug()
|
||||
.subscribe({
|
||||
|
@ -26,4 +34,17 @@ export class DebugComponent implements OnInit {
|
|||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
sendTestEmails () {
|
||||
this.debugService.testEmails(this.testEmail)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.testEmail = ''
|
||||
|
||||
this.notifier.success($localize`Emails will be sent!`)
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { catchError } from 'rxjs/operators'
|
|||
import { HttpClient } from '@angular/common/http'
|
||||
import { Injectable, inject } from '@angular/core'
|
||||
import { RestExtractor } from '@app/core'
|
||||
import { Debug } from '@peertube/peertube-models'
|
||||
import { Debug, SendDebugCommand } from '@peertube/peertube-models'
|
||||
import { environment } from '../../../../environments/environment'
|
||||
|
||||
@Injectable()
|
||||
|
@ -19,4 +19,16 @@ export class DebugService {
|
|||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
|
||||
testEmails (email: string) {
|
||||
const body: SendDebugCommand = {
|
||||
command: 'test-emails',
|
||||
email
|
||||
}
|
||||
|
||||
return this.authHttp.post(DebugService.BASE_DEBUG_URL + '/run-command', body)
|
||||
.pipe(
|
||||
catchError(err => this.restExtractor.handleError(err))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
<div class="pt-two-cols mt-5"> <!-- interface grid -->
|
||||
<div class="title-col">
|
||||
<div class="anchor" id="interface-settings"></div> <!-- interface settings anchor -->
|
||||
<h2 i18n>INTERFACE</h2>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { DOCUMENT, getLocaleDirection, NgClass, NgIf, PlatformLocation } from '@angular/common'
|
||||
import { AfterViewInit, Component, inject, LOCALE_ID, OnDestroy, OnInit, viewChild } from '@angular/core'
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
||||
import { ActivatedRoute, Event, GuardsCheckStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterOutlet } from '@angular/router'
|
||||
import { Event, GuardsCheckStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterOutlet } from '@angular/router'
|
||||
import {
|
||||
AuthService,
|
||||
Hotkey,
|
||||
|
@ -83,7 +83,6 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
private scrollService = inject(ScrollService)
|
||||
private userLocalStorage = inject(UserLocalStorageService)
|
||||
private peertubeModal = inject(PeertubeModalService)
|
||||
private route = inject(ActivatedRoute)
|
||||
|
||||
menu = inject(MenuService)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Routes, UrlMatchResult, UrlSegment } from '@angular/router'
|
||||
import { POSSIBLE_LOCALES } from '@peertube/peertube-core-utils'
|
||||
import { AVAILABLE_LOCALES } from '@peertube/peertube-core-utils'
|
||||
import { MetaGuard } from './core'
|
||||
import { EmptyComponent } from './empty.component'
|
||||
import { HomepageRedirectComponent } from './homepage-redirect.component'
|
||||
|
@ -241,7 +241,7 @@ const routes: Routes = [
|
|||
]
|
||||
|
||||
// Avoid 404 when changing language
|
||||
for (const locale of POSSIBLE_LOCALES) {
|
||||
for (const locale of AVAILABLE_LOCALES) {
|
||||
routes.push({
|
||||
path: locale,
|
||||
component: HomepageRedirectComponent
|
||||
|
|
|
@ -76,6 +76,8 @@ export class User implements UserServerModel {
|
|||
|
||||
twoFactorEnabled: boolean
|
||||
|
||||
language: string
|
||||
|
||||
createdAt: Date
|
||||
|
||||
constructor (hash: Partial<UserServerModel>) {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Injectable, LOCALE_ID, inject } from '@angular/core'
|
||||
import { AuthService } from '@app/core/auth'
|
||||
import { getCompleteLocale } from '@peertube/peertube-core-utils'
|
||||
import { ActorImage, User as UserServerModel, UserUpdateMe, UserVideoQuota } from '@peertube/peertube-models'
|
||||
import { Observable, of } from 'rxjs'
|
||||
import { catchError, first, map, shareReplay } from 'rxjs/operators'
|
||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Injectable, inject } from '@angular/core'
|
||||
import { AuthService } from '@app/core/auth'
|
||||
import { ActorImage, User as UserServerModel, UserUpdateMe, UserVideoQuota } from '@peertube/peertube-models'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { RestExtractor } from '../rest'
|
||||
import { UserLocalStorageService } from './user-local-storage.service'
|
||||
|
@ -14,9 +15,11 @@ export class UserService {
|
|||
private authHttp = inject(HttpClient)
|
||||
private authService = inject(AuthService)
|
||||
private restExtractor = inject(RestExtractor)
|
||||
private localeId = inject(LOCALE_ID)
|
||||
private userLocalStorageService = inject(UserLocalStorageService)
|
||||
|
||||
static BASE_USERS_URL = environment.apiUrl + '/api/v1/users/'
|
||||
static BASE_CLIENT_CONFIG_URL = environment.apiUrl + '/api/v1/client-config/'
|
||||
|
||||
private userCache: { [id: number]: Observable<UserServerModel> } = {}
|
||||
private signupInThisSession = false
|
||||
|
@ -60,7 +63,11 @@ export class UserService {
|
|||
}
|
||||
|
||||
getAnonymousUser () {
|
||||
return new User(this.userLocalStorageService.getUserInfo())
|
||||
return new User({
|
||||
...this.userLocalStorageService.getUserInfo(),
|
||||
|
||||
language: getCompleteLocale(this.localeId)
|
||||
})
|
||||
}
|
||||
|
||||
getAnonymousOrLoggedUser () {
|
||||
|
@ -188,4 +195,12 @@ export class UserService {
|
|||
.get<string[]>(url, { params })
|
||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||
}
|
||||
|
||||
updateInterfaceLanguage (language: string) {
|
||||
const url = UserService.BASE_CLIENT_CONFIG_URL + 'update-interface-language'
|
||||
const body = { language }
|
||||
|
||||
return this.authHttp.post(url, body)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,23 +65,20 @@
|
|||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<a
|
||||
*ngIf="user.account" ngbDropdownItem class="dropdown-item" routerLink="/my-account"
|
||||
#manageAccount
|
||||
>
|
||||
<a *ngIf="user.account" ngbDropdownItem class="dropdown-item" routerLink="/my-account">
|
||||
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon> <ng-container i18n>Manage my account</ng-container>
|
||||
</a>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<button
|
||||
<a
|
||||
myPluginSelector pluginSelectorId="menu-user-dropdown-language-item"
|
||||
ngbDropdownItem class="dropdown-item" (click)="openLanguageChooser()"
|
||||
ngbDropdownItem class="dropdown-item" routerLink="/my-account/settings" fragment="interface-settings"
|
||||
>
|
||||
<my-global-icon iconName="language" aria-hidden="true"></my-global-icon>
|
||||
<span i18n>Interface:</span>
|
||||
<span class="ms-auto ps-2 muted">{{ currentInterfaceLanguage }}</span>
|
||||
</button>
|
||||
</a>
|
||||
|
||||
<button *ngIf="!isInMobileView" ngbDropdownItem class="dropdown-item" (click)="openHotkeysCheatSheet()">
|
||||
<my-global-icon iconName="keyboard" aria-hidden="true"></my-global-icon>
|
||||
|
@ -100,5 +97,4 @@
|
|||
<my-button theme="tertiary" rounded="true" class="menu-button margin-button" icon="menu" (click)="toggleMenu()"></my-button>
|
||||
</div>
|
||||
|
||||
<my-language-chooser #languageChooserModal></my-language-chooser>
|
||||
<my-quick-settings #quickSettingsModal (openLanguageModal)="languageChooserModal.show()"></my-quick-settings>
|
||||
<my-quick-settings #quickSettingsModal></my-quick-settings>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnDestroy, OnInit, inject, viewChild } from '@angular/core'
|
||||
import { Component, inject, LOCALE_ID, OnDestroy, OnInit, viewChild } from '@angular/core'
|
||||
import { NavigationEnd, Router, RouterLink } from '@angular/router'
|
||||
import { AuthService, AuthStatus, AuthUser, HotkeysService, MenuService, RedirectService, ScreenService, ServerService } from '@app/core'
|
||||
import { NotificationDropdownComponent } from '@app/header/notification-dropdown.component'
|
||||
import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
|
||||
import { QuickSettingsModalComponent } from '@app/menu/quick-settings-modal.component'
|
||||
import { ActorAvatarComponent } from '@app/shared/shared-actor-image/actor-avatar.component'
|
||||
import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service'
|
||||
|
@ -11,15 +10,16 @@ import { PluginSelectorDirective } from '@app/shared/shared-main/plugins/plugin-
|
|||
import { LoginLinkComponent } from '@app/shared/shared-main/users/login-link.component'
|
||||
import { SignupLabelComponent } from '@app/shared/shared-main/users/signup-label.component'
|
||||
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { findAppropriateImage, getCompleteLocale, I18N_LOCALES } from '@peertube/peertube-core-utils'
|
||||
import { HTMLServerConfig, ServerConfig } from '@peertube/peertube-models'
|
||||
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
||||
import { isAndroid, isIOS, isIphone } from '@root-helpers/web-browser'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { GlobalIconComponent } from '../shared/shared-icons/global-icon.component'
|
||||
import { ButtonComponent } from '../shared/shared-main/buttons/button.component'
|
||||
import { SearchTypeaheadComponent } from './search-typeahead.component'
|
||||
import { HeaderService } from './header.service'
|
||||
import { findAppropriateImage } from '@peertube/peertube-core-utils'
|
||||
import { SearchTypeaheadComponent } from './search-typeahead.component'
|
||||
import { getDevLocale, isOnDevLocale } from '@app/helpers'
|
||||
|
||||
@Component({
|
||||
selector: 'my-header',
|
||||
|
@ -32,7 +32,6 @@ import { findAppropriateImage } from '@peertube/peertube-core-utils'
|
|||
PluginSelectorDirective,
|
||||
SignupLabelComponent,
|
||||
LoginLinkComponent,
|
||||
LanguageChooserComponent,
|
||||
QuickSettingsModalComponent,
|
||||
GlobalIconComponent,
|
||||
RouterLink,
|
||||
|
@ -53,10 +52,10 @@ export class HeaderComponent implements OnInit, OnDestroy {
|
|||
private router = inject(Router)
|
||||
private menu = inject(MenuService)
|
||||
private headerService = inject(HeaderService)
|
||||
private localeId = inject(LOCALE_ID)
|
||||
|
||||
private static LS_HIDE_MOBILE_MSG = 'hide-mobile-msg'
|
||||
|
||||
readonly languageChooserModal = viewChild<LanguageChooserComponent>('languageChooserModal')
|
||||
readonly quickSettingsModal = viewChild<QuickSettingsModalComponent>('quickSettingsModal')
|
||||
readonly dropdown = viewChild<NgbDropdown>('dropdown')
|
||||
|
||||
|
@ -65,8 +64,6 @@ export class HeaderComponent implements OnInit, OnDestroy {
|
|||
|
||||
hotkeysHelpVisible = false
|
||||
|
||||
currentInterfaceLanguage: string
|
||||
|
||||
mobileMsg = false
|
||||
androidAppUrl = ''
|
||||
iosAppUrl = ''
|
||||
|
@ -81,8 +78,15 @@ export class HeaderComponent implements OnInit, OnDestroy {
|
|||
private hotkeysSub: Subscription
|
||||
private authSub: Subscription
|
||||
|
||||
get language () {
|
||||
return this.languageChooserModal().getCurrentLanguage()
|
||||
get currentInterfaceLanguage () {
|
||||
const english = 'English'
|
||||
const locale = isOnDevLocale()
|
||||
? getDevLocale()
|
||||
: getCompleteLocale(this.localeId)
|
||||
|
||||
if (locale) return I18N_LOCALES[locale as keyof typeof I18N_LOCALES] || english
|
||||
|
||||
return english
|
||||
}
|
||||
|
||||
get requiresApproval () {
|
||||
|
@ -121,7 +125,6 @@ export class HeaderComponent implements OnInit, OnDestroy {
|
|||
|
||||
ngOnInit () {
|
||||
this.htmlConfig = this.serverService.getHTMLConfig()
|
||||
this.currentInterfaceLanguage = this.languageChooserModal().getCurrentLanguage()
|
||||
|
||||
this.loggedIn = this.authService.isLoggedIn()
|
||||
this.updateUserState()
|
||||
|
@ -273,10 +276,6 @@ export class HeaderComponent implements OnInit, OnDestroy {
|
|||
this.redirectService.redirectToHomepage()
|
||||
}
|
||||
|
||||
openLanguageChooser () {
|
||||
this.languageChooserModal().show()
|
||||
}
|
||||
|
||||
openQuickSettings () {
|
||||
this.quickSettingsModal().show()
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
<ng-template #modal let-hide="close">
|
||||
<div class="modal-header">
|
||||
<h4 i18n class="modal-title">Change the language</h4>
|
||||
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
|
||||
<my-global-icon iconName="cross"></my-global-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<a i18n class="peertube-button-link primary-button rounded-0" target="_blank" rel="noreferrer noopener" href="https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/translation.md">
|
||||
Help to translate PeerTube!
|
||||
</a>
|
||||
|
||||
<div class="modal-body">
|
||||
<a *ngFor="let lang of languages" [href]="buildLanguageLink(lang)" [lang]=lang.iso>{{ lang.label }}</a>
|
||||
</div>
|
||||
</ng-template>
|
|
@ -1,11 +0,0 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.modal-body {
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
margin: 15px;
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, ElementRef, LOCALE_ID, inject, viewChild } from '@angular/core'
|
||||
import { getDevLocale, isOnDevLocale } from '@app/helpers'
|
||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { getCompleteLocale, getShortLocale, I18N_LOCALES, objectKeysTyped, sortBy } from '@peertube/peertube-core-utils'
|
||||
|
||||
@Component({
|
||||
selector: 'my-language-chooser',
|
||||
templateUrl: './language-chooser.component.html',
|
||||
styleUrls: [ './language-chooser.component.scss' ],
|
||||
imports: [ CommonModule, GlobalIconComponent ]
|
||||
})
|
||||
export class LanguageChooserComponent {
|
||||
private modalService = inject(NgbModal)
|
||||
private localeId = inject(LOCALE_ID)
|
||||
|
||||
readonly modal = viewChild<ElementRef>('modal')
|
||||
|
||||
languages: { id: string, label: string, iso: string }[] = []
|
||||
|
||||
constructor () {
|
||||
const l = objectKeysTyped(I18N_LOCALES)
|
||||
.map(k => ({ id: k, label: I18N_LOCALES[k], iso: getShortLocale(k) }))
|
||||
|
||||
this.languages = sortBy(l, 'label')
|
||||
}
|
||||
|
||||
show () {
|
||||
this.modalService.open(this.modal(), { centered: true })
|
||||
}
|
||||
|
||||
buildLanguageLink (lang: { id: string }) {
|
||||
return window.location.origin + '/' + lang.id
|
||||
}
|
||||
|
||||
getCurrentLanguage () {
|
||||
const english = 'English'
|
||||
const locale = isOnDevLocale() ? getDevLocale() : getCompleteLocale(this.localeId)
|
||||
|
||||
if (locale) return I18N_LOCALES[locale as keyof typeof I18N_LOCALES] || english
|
||||
return english
|
||||
}
|
||||
}
|
|
@ -9,21 +9,20 @@
|
|||
<div class="modal-body">
|
||||
<my-alert i18n type="primary">These settings apply only to your session on this instance.</my-alert>
|
||||
|
||||
<h5 i18n class="section-label mt-4 mb-2">VIDEOS</h5>
|
||||
|
||||
<my-user-video-settings
|
||||
*ngIf="!isUserLoggedIn()"
|
||||
[user]="user" [userInformationLoaded]="userInformationLoaded" [reactiveUpdate]="true" [notifyOnUpdate]="true"
|
||||
>
|
||||
</my-user-video-settings>
|
||||
|
||||
<h5 i18n class="section-label mt-4 mb-2">INTERFACE</h5>
|
||||
<h5 i18n class="section-label mt-4 mb-3">INTERFACE</h5>
|
||||
|
||||
<my-user-interface-settings
|
||||
*ngIf="!isUserLoggedIn()"
|
||||
[user]="user" [userInformationLoaded]="userInformationLoaded" [reactiveUpdate]="true" [notifyOnUpdate]="true"
|
||||
[user]="user" [userInformationLoaded]="userInformationLoaded" reactiveUpdate="true" notifyOnUpdate="true"
|
||||
></my-user-interface-settings>
|
||||
|
||||
<my-button i18n class="mt-2" theme="secondary" icon="language" (click)="changeLanguage()">Change interface language</my-button>
|
||||
|
||||
<h5 i18n class="section-label mt-4 mb-3">VIDEOS</h5>
|
||||
|
||||
<my-user-video-settings
|
||||
*ngIf="!isUserLoggedIn()"
|
||||
[user]="user" [userInformationLoaded]="userInformationLoaded" reactiveUpdate="true" notifyOnUpdate="true"
|
||||
>
|
||||
</my-user-video-settings>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { CommonModule } from '@angular/common'
|
||||
import { Component, OnDestroy, OnInit, inject, output, viewChild } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit, inject, viewChild } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AuthService, AuthStatus, LocalStorageService, PeerTubeRouterService, User, UserService } from '@app/core'
|
||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
||||
import { ButtonComponent } from '@app/shared/shared-main/buttons/button.component'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { UserInterfaceSettingsComponent } from '@app/shared/shared-user-settings/user-interface-settings.component'
|
||||
import { UserVideoSettingsComponent } from '@app/shared/shared-user-settings/user-video-settings.component'
|
||||
|
@ -15,13 +14,17 @@ import { filter } from 'rxjs/operators'
|
|||
@Component({
|
||||
selector: 'my-quick-settings',
|
||||
templateUrl: './quick-settings-modal.component.html',
|
||||
styles: [
|
||||
`h5 {
|
||||
font-size: 1rem;
|
||||
}`
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GlobalIconComponent,
|
||||
UserVideoSettingsComponent,
|
||||
UserInterfaceSettingsComponent,
|
||||
AlertComponent,
|
||||
ButtonComponent
|
||||
AlertComponent
|
||||
]
|
||||
})
|
||||
export class QuickSettingsModalComponent implements OnInit, OnDestroy {
|
||||
|
@ -36,8 +39,6 @@ export class QuickSettingsModalComponent implements OnInit, OnDestroy {
|
|||
|
||||
readonly modal = viewChild<NgbModal>('modal')
|
||||
|
||||
readonly openLanguageModal = output()
|
||||
|
||||
user: User
|
||||
userInformationLoaded = new ReplaySubject<boolean>(1)
|
||||
|
||||
|
@ -89,11 +90,6 @@ export class QuickSettingsModalComponent implements OnInit, OnDestroy {
|
|||
this.setModalQuery('add')
|
||||
}
|
||||
|
||||
changeLanguage () {
|
||||
this.openedModal.close()
|
||||
this.openLanguageModal.emit()
|
||||
}
|
||||
|
||||
private setModalQuery (type: 'add' | 'remove') {
|
||||
const modal = type === 'add'
|
||||
? QuickSettingsModalComponent.QUERY_MODAL_NAME
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { HttpHandlerFn, HttpRequest } from '@angular/common/http'
|
||||
import { inject, LOCALE_ID } from '@angular/core'
|
||||
|
||||
export function languageInterceptor (req: HttpRequest<unknown>, next: HttpHandlerFn) {
|
||||
const localeId = inject(LOCALE_ID)
|
||||
|
||||
const newReq = req.clone({ headers: req.headers.append('x-peertube-language', localeId) })
|
||||
|
||||
return next(newReq)
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
import { DatePipe } from '@angular/common'
|
||||
import { AccountService } from './account/account.service'
|
||||
import { AUTH_INTERCEPTOR_PROVIDER } from './auth/auth-interceptor.service'
|
||||
import { VideoChannelSyncService } from './channel/video-channel-sync.service'
|
||||
import { VideoChannelService } from './channel/video-channel.service'
|
||||
import { CustomPageService } from './custom-page/custom-page.service'
|
||||
import { FromNowPipe } from './date/from-now.pipe'
|
||||
import { AUTH_INTERCEPTOR_PROVIDER } from './http/auth-interceptor.service'
|
||||
import { InstanceService } from './instance/instance.service'
|
||||
import { ActorRedirectGuard } from './router/actor-redirect-guard.service'
|
||||
import { UserHistoryService } from './users/user-history.service'
|
||||
|
|
|
@ -1,4 +1,21 @@
|
|||
<form (ngSubmit)="updateInterfaceSettings()" [formGroup]="form">
|
||||
<div class="form-group">
|
||||
<label i18n for="language">Language</label>
|
||||
<div class="form-group-description">
|
||||
<div i18n>Preferred language for the web interface and for emails</div>
|
||||
|
||||
<div i18n>
|
||||
You can help us to translate the platform by consulting <a
|
||||
class="link-primary"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/translation.md#how-to"
|
||||
>this guide</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<my-select-options inputId="language" formControlName="language" [items]="availableLanguages"></my-select-options>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="theme">Theme</label>
|
||||
|
@ -6,5 +23,5 @@
|
|||
<my-select-options inputId="theme" formControlName="theme" [items]="availableThemes"></my-select-options>
|
||||
</div>
|
||||
|
||||
<input *ngIf="!reactiveUpdate()" type="submit" class="peertube-button primary-button" i18n-value value="Save interface settings" [disabled]="!form.valid">
|
||||
<input *ngIf="!reactiveUpdate()" type="submit" class="peertube-button primary-button" [value]="getSubmitValue()" [disabled]="!form.valid">
|
||||
</form>
|
||||
|
|
|
@ -1,37 +1,56 @@
|
|||
import { Subject, Subscription } from 'rxjs'
|
||||
import { Component, OnDestroy, OnInit, inject, input } from '@angular/core'
|
||||
import { AuthService, Notifier, ServerService, ThemeService, UserService } from '@app/core'
|
||||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { HTMLServerConfig, User, UserUpdateMe } from '@peertube/peertube-models'
|
||||
import { SelectOptionsItem } from 'src/types'
|
||||
import { NgIf } from '@angular/common'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { booleanAttribute, Component, inject, input, OnDestroy, OnInit } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { AuthService, Notifier, ServerService, ThemeService, UserService } from '@app/core'
|
||||
import {
|
||||
BuildFormArgumentTyped,
|
||||
FormReactiveErrorsTyped,
|
||||
FormReactiveMessagesTyped,
|
||||
FormReactiveService
|
||||
} from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { I18N_LOCALES } from '@peertube/peertube-core-utils'
|
||||
import { HTMLServerConfig, User, UserUpdateMe } from '@peertube/peertube-models'
|
||||
import { of, Subject, Subscription, switchMap } from 'rxjs'
|
||||
import { SelectOptionsItem } from 'src/types'
|
||||
import { SelectOptionsComponent } from '../shared-forms/select/select-options.component'
|
||||
|
||||
type Form = {
|
||||
theme: FormControl<string>
|
||||
language: FormControl<string>
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-user-interface-settings',
|
||||
templateUrl: './user-interface-settings.component.html',
|
||||
styleUrls: [ './user-interface-settings.component.scss' ],
|
||||
imports: [ FormsModule, ReactiveFormsModule, NgIf, SelectOptionsComponent ]
|
||||
})
|
||||
export class UserInterfaceSettingsComponent extends FormReactive implements OnInit, OnDestroy {
|
||||
protected formReactiveService = inject(FormReactiveService)
|
||||
export class UserInterfaceSettingsComponent implements OnInit, OnDestroy {
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
|
||||
private authService = inject(AuthService)
|
||||
private notifier = inject(Notifier)
|
||||
private userService = inject(UserService)
|
||||
private themeService = inject(ThemeService)
|
||||
private serverService = inject(ServerService)
|
||||
|
||||
readonly user = input<User>(undefined)
|
||||
readonly reactiveUpdate = input(false)
|
||||
readonly notifyOnUpdate = input(true)
|
||||
readonly user = input<Pick<User, 'theme' | 'language'>>(undefined)
|
||||
readonly reactiveUpdate = input(false, { transform: booleanAttribute })
|
||||
readonly notifyOnUpdate = input(true, { transform: booleanAttribute })
|
||||
readonly userInformationLoaded = input<Subject<any>>(undefined)
|
||||
|
||||
form: FormGroup<Form>
|
||||
formErrors: FormReactiveErrorsTyped<Form> = {}
|
||||
validationMessages: FormReactiveMessagesTyped<Form> = {}
|
||||
|
||||
availableThemes: SelectOptionsItem[]
|
||||
availableLanguages: SelectOptionsItem[]
|
||||
|
||||
formValuesWatcher: Subscription
|
||||
|
||||
private serverConfig: HTMLServerConfig
|
||||
private initialUserLanguage: string
|
||||
private updating = false
|
||||
|
||||
get instanceName () {
|
||||
return this.serverConfig.instance.name
|
||||
|
@ -39,6 +58,7 @@ export class UserInterfaceSettingsComponent extends FormReactive implements OnIn
|
|||
|
||||
ngOnInit () {
|
||||
this.serverConfig = this.serverService.getHTMLConfig()
|
||||
this.initialUserLanguage = this.user().language
|
||||
|
||||
this.availableThemes = [
|
||||
{ id: 'instance-default', label: $localize`${this.instanceName} theme`, description: this.getDefaultInstanceThemeLabel() },
|
||||
|
@ -48,14 +68,15 @@ export class UserInterfaceSettingsComponent extends FormReactive implements OnIn
|
|||
...this.themeService.buildAvailableThemes()
|
||||
]
|
||||
|
||||
this.buildForm({
|
||||
theme: null
|
||||
})
|
||||
this.availableLanguages = Object.entries(I18N_LOCALES).map(([ id, label ]) => ({ label, id }))
|
||||
|
||||
this.buildForm()
|
||||
|
||||
this.userInformationLoaded()
|
||||
.subscribe(() => {
|
||||
this.form.patchValue({
|
||||
theme: this.user().theme
|
||||
theme: this.user().theme,
|
||||
language: this.user().language
|
||||
})
|
||||
|
||||
if (this.reactiveUpdate()) {
|
||||
|
@ -63,23 +84,57 @@ export class UserInterfaceSettingsComponent extends FormReactive implements OnIn
|
|||
}
|
||||
})
|
||||
}
|
||||
private buildForm () {
|
||||
const obj: BuildFormArgumentTyped<Form> = {
|
||||
theme: null,
|
||||
language: null
|
||||
}
|
||||
|
||||
const {
|
||||
form,
|
||||
formErrors,
|
||||
validationMessages
|
||||
} = this.formReactiveService.buildForm<Form>(obj)
|
||||
|
||||
this.form = form
|
||||
this.formErrors = formErrors
|
||||
this.validationMessages = validationMessages
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.formValuesWatcher?.unsubscribe()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
updateInterfaceSettings () {
|
||||
const theme = this.form.value['theme']
|
||||
if (this.updating) return
|
||||
this.updating = true
|
||||
|
||||
const { theme, language } = this.form.value
|
||||
|
||||
const details: UserUpdateMe = {
|
||||
theme
|
||||
theme,
|
||||
language
|
||||
}
|
||||
const changedLanguage = language !== this.initialUserLanguage
|
||||
|
||||
const changeLanguageObs = changedLanguage
|
||||
? this.userService.updateInterfaceLanguage(details.language)
|
||||
: of(true)
|
||||
|
||||
if (this.authService.isLoggedIn()) {
|
||||
this.userService.updateMyProfile(details)
|
||||
.pipe(switchMap(() => changeLanguageObs))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
if (changedLanguage) {
|
||||
window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
this.authService.refreshUserInformation()
|
||||
this.updating = false
|
||||
|
||||
if (this.notifyOnUpdate()) this.notifier.success($localize`Interface settings updated.`)
|
||||
},
|
||||
|
@ -91,7 +146,26 @@ export class UserInterfaceSettingsComponent extends FormReactive implements OnIn
|
|||
}
|
||||
|
||||
this.userService.updateMyAnonymousProfile(details)
|
||||
if (changedLanguage) {
|
||||
changeLanguageObs.subscribe({
|
||||
next: () => {
|
||||
window.location.reload()
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.notifyOnUpdate()) this.notifier.success($localize`Interface settings updated.`)
|
||||
this.updating = false
|
||||
}
|
||||
|
||||
getSubmitValue () {
|
||||
return $localize`Save interface settings`
|
||||
|
||||
// return $localize`Save and reload the interface`
|
||||
}
|
||||
|
||||
private getDefaultInstanceThemeLabel () {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { NgIf } from '@angular/common'
|
||||
import { Component, OnDestroy, OnInit, inject, input } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit, booleanAttribute, inject, input } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { AuthService, Notifier, ServerService, User, UserService } from '@app/core'
|
||||
import { FormReactiveErrors, FormReactiveService, FormReactiveMessages } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { FormReactiveErrors, FormReactiveMessages, FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { NSFWFlag, NSFWFlagType, NSFWPolicyType, UserUpdateMe } from '@peertube/peertube-models'
|
||||
import { pick } from 'lodash-es'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
|
@ -49,8 +49,8 @@ export class UserVideoSettingsComponent implements OnInit, OnDestroy {
|
|||
private serverService = inject(ServerService)
|
||||
|
||||
readonly user = input<User>(null)
|
||||
readonly reactiveUpdate = input(false)
|
||||
readonly notifyOnUpdate = input(true)
|
||||
readonly reactiveUpdate = input(false, { transform: booleanAttribute })
|
||||
readonly notifyOnUpdate = input(true, { transform: booleanAttribute })
|
||||
readonly userInformationLoaded = input<Subject<any>>(undefined)
|
||||
|
||||
form: FormGroup<Form>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { APP_BASE_HREF, registerLocaleData } from '@angular/common'
|
||||
import { provideHttpClient } from '@angular/common/http'
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http'
|
||||
import {
|
||||
ApplicationRef,
|
||||
enableProdMode,
|
||||
|
@ -15,6 +15,7 @@ import { ServiceWorkerModule } from '@angular/service-worker'
|
|||
import { PTPrimeTheme } from '@app/core/theme/primeng/primeng-theme'
|
||||
import localeOc from '@app/helpers/locales/oc'
|
||||
import { getFormProviders } from '@app/shared/shared-forms/shared-form-providers'
|
||||
import { languageInterceptor } from '@app/shared/shared-main/http/language-interceptor.service'
|
||||
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { LoadingBarModule } from '@ngx-loading-bar/core'
|
||||
import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
|
||||
|
@ -76,7 +77,9 @@ const bootstrap = () =>
|
|||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
|
||||
),
|
||||
|
||||
provideHttpClient(),
|
||||
provideHttpClient(
|
||||
withInterceptors([ languageInterceptor ])
|
||||
),
|
||||
|
||||
importProvidersFrom(
|
||||
LoadingBarHttpClientModule,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@use '_icons' as *;
|
||||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_button-mixins' as *;
|
||||
@use "_icons" as *;
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_button-mixins" as *;
|
||||
|
||||
.no-results {
|
||||
height: 40vh;
|
||||
|
@ -68,13 +68,12 @@
|
|||
|
||||
.anchor {
|
||||
position: relative;
|
||||
top: -calc(#{pvar(--header-height)} + 20px);
|
||||
top: calc((#{pvar(--header-height)} + 20px) * -1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.alert {
|
||||
|
||||
&.pt-alert-primary {
|
||||
color: pvar(--alert-primary-fg);
|
||||
background-color: pvar(--alert-primary-bg);
|
||||
|
|
|
@ -62,7 +62,7 @@ export class PeerTubeEmbed {
|
|||
constructor (videoWrapperId: string) {
|
||||
logger.registerServerSending(getBackendUrl())
|
||||
|
||||
this.http = new AuthHTTP(getBackendUrl())
|
||||
this.http = new AuthHTTP(getBackendUrl(), navigator.language)
|
||||
|
||||
this.videoFetcher = new VideoFetcher(this.http)
|
||||
this.playlistFetcher = new PlaylistFetcher(this.http)
|
||||
|
|
|
@ -12,7 +12,7 @@ export class AuthHTTP {
|
|||
|
||||
private headers = new Headers()
|
||||
|
||||
constructor (private readonly serverUrl: string) {
|
||||
constructor (private readonly serverUrl: string, private readonly language: string) {
|
||||
this.userOAuthTokens = OAuthUserTokens.getUserTokens(peertubeLocalStorage)
|
||||
|
||||
if (this.userOAuthTokens) this.setHeadersFromTokens()
|
||||
|
@ -22,6 +22,8 @@ export class AuthHTTP {
|
|||
let refreshFetchOptions: { headers?: Headers } = {}
|
||||
|
||||
if (isSameOrigin(this.serverUrl, url)) {
|
||||
if (this.language) this.headers.set('x-peertube-language', this.language)
|
||||
|
||||
if (videoPassword) this.headers.set('x-peertube-video-password', videoPassword)
|
||||
|
||||
if (videoPassword || optionalAuth) refreshFetchOptions = { headers: this.headers }
|
||||
|
|
|
@ -893,6 +893,10 @@ instance:
|
|||
# Example: '2 vCore, 2GB RAM...'
|
||||
hardware_information: '' # Supports Markdown
|
||||
|
||||
# Default language of your instance, used in emails for example
|
||||
# The web interface still uses the web browser preferred language
|
||||
default_language: 'en'
|
||||
|
||||
# Describe the languages spoken on your instance, to interact with your users for example
|
||||
# Uncomment or add the languages you want
|
||||
# List of supported languages: https://peertube.cpy.re/api/v1/videos/languages
|
||||
|
|
|
@ -557,7 +557,7 @@ signup:
|
|||
user:
|
||||
history:
|
||||
videos:
|
||||
# Enable or disable video history by default for new users.
|
||||
# Enable or disable video history by default for new users
|
||||
enabled: true
|
||||
|
||||
# Default value of maximum video bytes the user can upload
|
||||
|
@ -903,6 +903,10 @@ instance:
|
|||
# Example: '2 vCore, 2GB RAM...'
|
||||
hardware_information: '' # Supports Markdown
|
||||
|
||||
# Default language of your instance, used in emails for example
|
||||
# The web interface still uses the web browser preferred language
|
||||
default_language: 'en'
|
||||
|
||||
# Describe the languages spoken on your instance, to interact with your users for example
|
||||
# Uncomment or add the languages you want
|
||||
# List of supported languages: https://peertube.cpy.re/api/v1/videos/languages
|
||||
|
|
|
@ -13,7 +13,8 @@ export default defineConfig([
|
|||
'packages/types-generator',
|
||||
'*.js',
|
||||
'client',
|
||||
'dist'
|
||||
'dist',
|
||||
'server/.i18next-parser.config.ts'
|
||||
]),
|
||||
|
||||
{
|
||||
|
|
|
@ -146,6 +146,9 @@
|
|||
"got-ssrf": "^3.0.0",
|
||||
"helmet": "^8.0.0",
|
||||
"http-problem-details": "^0.1.5",
|
||||
"i18next": "^25.3.2",
|
||||
"i18next-icu": "^2.3.0",
|
||||
"intl-messageformat": "^10.7.16",
|
||||
"ioredis": "^5.2.3",
|
||||
"ip-anonymize": "^0.1.0",
|
||||
"ipaddr.js": "2.2.0",
|
||||
|
@ -240,8 +243,11 @@
|
|||
"eslint": "^9.26.0",
|
||||
"eslint-config-love": "^119.0.0",
|
||||
"fast-xml-parser": "^5.2.2",
|
||||
"handlebars": "^4.7.8",
|
||||
"i18next-parser": "^9.3.0",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"jszip": "^3.10.1",
|
||||
"maildev": "^2.2.1",
|
||||
"mocha": "^11.1.0",
|
||||
"pixelmatch": "^7.1.0",
|
||||
"pngjs": "^7.0.0",
|
||||
|
|
|
@ -79,7 +79,7 @@ const I18N_LOCALE_ALIAS = {
|
|||
'zh': 'zh-Hans-CN'
|
||||
}
|
||||
|
||||
export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES).concat(Object.keys(I18N_LOCALE_ALIAS))
|
||||
export const AVAILABLE_LOCALES = Object.keys(I18N_LOCALES).concat(Object.keys(I18N_LOCALE_ALIAS))
|
||||
|
||||
export function getDefaultLocale () {
|
||||
return 'en-US'
|
||||
|
@ -95,13 +95,13 @@ export function peertubeTranslate (str: string, translations?: { [id: string]: s
|
|||
return translations[str]
|
||||
}
|
||||
|
||||
const possiblePaths = POSSIBLE_LOCALES.map(l => '/' + l)
|
||||
const possiblePaths = AVAILABLE_LOCALES.map(l => '/' + l)
|
||||
export function is18nPath (path: string) {
|
||||
return possiblePaths.includes(path)
|
||||
}
|
||||
|
||||
export function is18nLocale (locale: string) {
|
||||
return POSSIBLE_LOCALES.includes(locale)
|
||||
return AVAILABLE_LOCALES.includes(locale)
|
||||
}
|
||||
|
||||
export function getCompleteLocale (locale: string) {
|
||||
|
|
|
@ -42,6 +42,16 @@ export function getDefaultSanitizeOptions () {
|
|||
}
|
||||
}
|
||||
|
||||
export function getMailHtmlSanitizeOptions () {
|
||||
return {
|
||||
allowedTags: [ 'a', 'strong' ],
|
||||
allowedSchemes: getDefaultSanitizedSchemes(),
|
||||
allowedAttributes: {
|
||||
a: [ 'href', 'title' ]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getTextOnlySanitizeOptions () {
|
||||
return {
|
||||
allowedTags: [] as string[]
|
||||
|
@ -56,7 +66,7 @@ export function getTextOnlySanitizeOptions () {
|
|||
export function escapeHTML (stringParam: string) {
|
||||
if (!stringParam) return ''
|
||||
|
||||
const entityMap: { [id: string ]: string } = {
|
||||
const entityMap: { [id: string]: string } = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
|
|
|
@ -15,6 +15,7 @@ export interface UserSettingsExportJSON {
|
|||
|
||||
videosHistoryEnabled: boolean
|
||||
videoLanguages: string[]
|
||||
language: string
|
||||
|
||||
theme: string
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@ export interface CustomConfig {
|
|||
businessModel: string
|
||||
hardwareInformation: string
|
||||
|
||||
defaultLanguage: string
|
||||
|
||||
languages: string[]
|
||||
categories: number[]
|
||||
|
||||
|
|
|
@ -3,11 +3,17 @@ export interface Debug {
|
|||
activityPubMessagesWaiting: number
|
||||
}
|
||||
|
||||
export interface SendDebugCommand {
|
||||
command: 'remove-dandling-resumable-uploads'
|
||||
| 'process-video-views-buffer'
|
||||
| 'process-video-viewers'
|
||||
| 'process-video-channel-sync-latest'
|
||||
| 'process-update-videos-scheduler'
|
||||
| 'remove-expired-user-exports'
|
||||
export type SendDebugCommand = {
|
||||
command:
|
||||
| 'remove-dandling-resumable-uploads'
|
||||
| 'process-video-views-buffer'
|
||||
| 'process-video-viewers'
|
||||
| 'process-video-channel-sync-latest'
|
||||
| 'process-update-videos-scheduler'
|
||||
| 'remove-expired-user-exports'
|
||||
} | SendDebugTestEmails
|
||||
|
||||
export type SendDebugTestEmails = {
|
||||
command: 'test-emails'
|
||||
email: string
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
export type To = { email: string, language: string }
|
||||
type From = string | { name?: string, address: string }
|
||||
|
||||
interface Base extends Partial<SendEmailDefaultMessageOptions> {
|
||||
to: string[] | string
|
||||
interface Base {
|
||||
to: To[] | To
|
||||
|
||||
from?: From
|
||||
subject?: string
|
||||
replyTo?: string
|
||||
}
|
||||
|
||||
interface MailTemplate extends Base {
|
||||
|
@ -30,6 +35,8 @@ interface SendEmailDefaultLocalsOptions {
|
|||
fg: string
|
||||
bg: string
|
||||
primary: string
|
||||
language: string
|
||||
logoUrl: string
|
||||
}
|
||||
|
||||
interface SendEmailDefaultMessageOptions {
|
||||
|
|
|
@ -137,6 +137,8 @@ export interface ServerConfig {
|
|||
avatars: ActorImage[]
|
||||
banners: ActorImage[]
|
||||
|
||||
defaultLanguage: string
|
||||
|
||||
logo: {
|
||||
type: LogoType
|
||||
width: number
|
||||
|
|
|
@ -17,6 +17,7 @@ export interface UserUpdateMe {
|
|||
autoPlayNextVideoPlaylist?: boolean
|
||||
videosHistoryEnabled?: boolean
|
||||
videoLanguages?: string[]
|
||||
language?: string
|
||||
|
||||
email?: string
|
||||
emailPublic?: boolean
|
||||
|
|
|
@ -31,6 +31,7 @@ export interface User {
|
|||
|
||||
videosHistoryEnabled: boolean
|
||||
videoLanguages: string[]
|
||||
language: string
|
||||
|
||||
role: {
|
||||
id: UserRoleType
|
||||
|
|
|
@ -18,10 +18,11 @@ import { unwrapBody } from '../requests/index.js'
|
|||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class UsersCommand extends AbstractCommand {
|
||||
|
||||
askResetPassword (options: OverrideCommandOptions & {
|
||||
email: string
|
||||
}) {
|
||||
askResetPassword (
|
||||
options: OverrideCommandOptions & {
|
||||
email: string
|
||||
}
|
||||
) {
|
||||
const { email } = options
|
||||
const path = '/api/v1/users/ask-reset-password'
|
||||
|
||||
|
@ -35,11 +36,13 @@ export class UsersCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
resetPassword (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
verificationString: string
|
||||
password: string
|
||||
}) {
|
||||
resetPassword (
|
||||
options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
verificationString: string
|
||||
password: string
|
||||
}
|
||||
) {
|
||||
const { userId, verificationString, password } = options
|
||||
const path = '/api/v1/users/' + userId + '/reset-password'
|
||||
|
||||
|
@ -55,9 +58,11 @@ export class UsersCommand extends AbstractCommand {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
askSendVerifyEmail (options: OverrideCommandOptions & {
|
||||
email: string
|
||||
}) {
|
||||
askSendVerifyEmail (
|
||||
options: OverrideCommandOptions & {
|
||||
email: string
|
||||
}
|
||||
) {
|
||||
const { email } = options
|
||||
const path = '/api/v1/users/ask-send-verify-email'
|
||||
|
||||
|
@ -71,11 +76,13 @@ export class UsersCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
verifyEmail (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
verificationString: string
|
||||
isPendingEmail?: boolean // default false
|
||||
}) {
|
||||
verifyEmail (
|
||||
options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
verificationString: string
|
||||
isPendingEmail?: boolean // default false
|
||||
}
|
||||
) {
|
||||
const { userId, verificationString, isPendingEmail = false } = options
|
||||
const path = '/api/v1/users/' + userId + '/verify-email'
|
||||
|
||||
|
@ -94,10 +101,12 @@ export class UsersCommand extends AbstractCommand {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
banUser (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
reason?: string
|
||||
}) {
|
||||
banUser (
|
||||
options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
reason?: string
|
||||
}
|
||||
) {
|
||||
const { userId, reason } = options
|
||||
const path = '/api/v1/users' + '/' + userId + '/block'
|
||||
|
||||
|
@ -111,9 +120,11 @@ export class UsersCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
unbanUser (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
}) {
|
||||
unbanUser (
|
||||
options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
}
|
||||
) {
|
||||
const { userId } = options
|
||||
const path = '/api/v1/users' + '/' + userId + '/unblock'
|
||||
|
||||
|
@ -154,15 +165,17 @@ export class UsersCommand extends AbstractCommand {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
create (options: OverrideCommandOptions & {
|
||||
username: string
|
||||
password?: string
|
||||
videoQuota?: number
|
||||
videoQuotaDaily?: number
|
||||
role?: UserRoleType
|
||||
adminFlags?: UserAdminFlagType
|
||||
email?: string
|
||||
}) {
|
||||
create (
|
||||
options: OverrideCommandOptions & {
|
||||
username: string
|
||||
password?: string
|
||||
videoQuota?: number
|
||||
videoQuotaDaily?: number
|
||||
role?: UserRoleType
|
||||
adminFlags?: UserAdminFlagType
|
||||
email?: string
|
||||
}
|
||||
) {
|
||||
const {
|
||||
username,
|
||||
adminFlags,
|
||||
|
@ -243,9 +256,11 @@ export class UsersCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
getMyRating (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
getMyRating (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}
|
||||
) {
|
||||
const { videoId } = options
|
||||
const path = '/api/v1/users/me/videos/' + videoId + '/rating'
|
||||
|
||||
|
@ -285,9 +300,11 @@ export class UsersCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
updateMyAvatar (options: OverrideCommandOptions & {
|
||||
fixture: string
|
||||
}) {
|
||||
updateMyAvatar (
|
||||
options: OverrideCommandOptions & {
|
||||
fixture: string
|
||||
}
|
||||
) {
|
||||
const { fixture } = options
|
||||
const path = '/api/v1/users/me/avatar/pick'
|
||||
|
||||
|
@ -305,10 +322,12 @@ export class UsersCommand extends AbstractCommand {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
get (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
withStats?: boolean // default false
|
||||
}) {
|
||||
get (
|
||||
options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
withStats?: boolean // default false
|
||||
}
|
||||
) {
|
||||
const { userId, withStats } = options
|
||||
const path = '/api/v1/users/' + userId
|
||||
|
||||
|
@ -341,9 +360,11 @@ export class UsersCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
remove (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
}) {
|
||||
remove (
|
||||
options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
}
|
||||
) {
|
||||
const { userId } = options
|
||||
const path = '/api/v1/users/' + userId
|
||||
|
||||
|
@ -356,17 +377,19 @@ export class UsersCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
update (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
email?: string
|
||||
emailVerified?: boolean
|
||||
videoQuota?: number
|
||||
videoQuotaDaily?: number
|
||||
password?: string
|
||||
adminFlags?: UserAdminFlagType
|
||||
pluginAuth?: string
|
||||
role?: UserRoleType
|
||||
}) {
|
||||
update (
|
||||
options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
email?: string
|
||||
emailVerified?: boolean
|
||||
videoQuota?: number
|
||||
videoQuotaDaily?: number
|
||||
password?: string
|
||||
adminFlags?: UserAdminFlagType
|
||||
pluginAuth?: string
|
||||
role?: UserRoleType
|
||||
}
|
||||
) {
|
||||
const path = '/api/v1/users/' + options.userId
|
||||
|
||||
const toSend: UserUpdate = {}
|
||||
|
@ -388,4 +411,24 @@ export class UsersCommand extends AbstractCommand {
|
|||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
updateInterfaceLanguage (
|
||||
options: OverrideCommandOptions & {
|
||||
language: string
|
||||
}
|
||||
) {
|
||||
const { language } = options
|
||||
const path = '/api/v1/client-config/update-interface-language'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { language },
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
|
@ -10,6 +9,7 @@ import {
|
|||
killallServers,
|
||||
PeerTubeServer
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
|
||||
|
||||
describe('Test contact form API validators', function () {
|
||||
let server: PeerTubeServer
|
||||
|
@ -79,7 +79,7 @@ describe('Test contact form API validators', function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
|
|
|
@ -253,6 +253,12 @@ describe('Test my user API validators', function () {
|
|||
}
|
||||
})
|
||||
|
||||
it('Should fail with an invalid language attribute', async function () {
|
||||
const fields = { language: 'toto' }
|
||||
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
})
|
||||
|
||||
it('Should fail with an invalid theme', async function () {
|
||||
const fields = { theme: 'invalid' }
|
||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
|
||||
|
@ -293,7 +299,8 @@ describe('Test my user API validators', function () {
|
|||
theme: 'default',
|
||||
noInstanceConfigWarningModal: true,
|
||||
noWelcomeModal: true,
|
||||
noAccountSetupWarningModal: true
|
||||
noAccountSetupWarningModal: true,
|
||||
language: 'fr'
|
||||
}
|
||||
|
||||
await makePutBodyRequest({
|
||||
|
@ -545,6 +552,22 @@ describe('Test my user API validators', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Client config', function () {
|
||||
it('Should fail with an invalid language', async function () {
|
||||
await server.users.updateInterfaceLanguage({
|
||||
language: 'hello',
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed to update language with the correct params', async function () {
|
||||
await server.users.updateInterfaceLanguage({
|
||||
language: 'fr',
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When deleting our account', function () {
|
||||
it('Should fail with with the root account', async function () {
|
||||
await server.users.deleteMe({ expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
|
@ -552,7 +575,7 @@ describe('Test my user API validators', function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
|
|
|
@ -479,7 +479,7 @@ describe('Test users admin API validators', function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
|
|
|
@ -60,7 +60,6 @@ describe('Test admin notifications', function () {
|
|||
})
|
||||
|
||||
describe('Latest PeerTube version notification', function () {
|
||||
|
||||
it('Should not send a notification to admins if there is no new version', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
|
@ -104,7 +103,6 @@ describe('Test admin notifications', function () {
|
|||
})
|
||||
|
||||
describe('Latest plugin version notification', function () {
|
||||
|
||||
it('Should not send a notification to admins if there is no new plugin version', async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
|
@ -146,7 +144,7 @@ describe('Test admin notifications', function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await sqlCommand.cleanup()
|
||||
await cleanupTests([ server ])
|
||||
|
|
|
@ -3,11 +3,7 @@
|
|||
import { UserNotification } from '@peertube/peertube-models'
|
||||
import { PeerTubeServer, cleanupTests, waitJobs } from '@peertube/peertube-server-commands'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||
import {
|
||||
CheckerBaseParams,
|
||||
checkMyVideoTranscriptionGenerated,
|
||||
prepareNotificationsTest
|
||||
} from '@tests/shared/notifications.js'
|
||||
import { CheckerBaseParams, checkMyVideoTranscriptionGenerated, prepareNotificationsTest } from '@tests/shared/notifications.js'
|
||||
import { join } from 'path'
|
||||
|
||||
describe('Test caption notifications', function () {
|
||||
|
@ -74,7 +70,7 @@ describe('Test caption notifications', function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
|
|
|
@ -15,7 +15,7 @@ describe('Test comments notifications', function () {
|
|||
|
||||
const commentText = '**hello** <a href="https://joinpeertube.org">world</a>, <h1>what do you think about peertube?</h1>'
|
||||
const expectedHtml = '<strong>hello</strong> <a href="https://joinpeertube.org" target="_blank" rel="noopener noreferrer">world</a>' +
|
||||
', </p>what do you think about peertube?'
|
||||
', </p>what do you think about peertube?'
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
@ -392,7 +392,7 @@ describe('Test comments notifications', function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import './admin-notifications.js'
|
||||
import './captions-notifications.js'
|
||||
import './caption-notifications.js'
|
||||
import './comments-notifications.js'
|
||||
import './moderation-notifications.js'
|
||||
import './notifications-api.js'
|
||||
|
|
|
@ -427,7 +427,6 @@ describe('Test moderation notifications', function () {
|
|||
let videoName: string
|
||||
|
||||
before(async function () {
|
||||
|
||||
adminBaseParamsServer1 = {
|
||||
server: servers[0],
|
||||
emails,
|
||||
|
@ -583,7 +582,7 @@ describe('Test moderation notifications', function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
|
|
|
@ -36,7 +36,6 @@ describe('Test notifications API', function () {
|
|||
})
|
||||
|
||||
describe('Notification list & count', function () {
|
||||
|
||||
it('Should correctly list notifications', async function () {
|
||||
const { data, total } = await server.notifications.list({ token: userToken, start: 0, count: 2 })
|
||||
|
||||
|
@ -74,7 +73,6 @@ describe('Test notifications API', function () {
|
|||
})
|
||||
|
||||
describe('Mark as read', function () {
|
||||
|
||||
it('Should mark as read some notifications', async function () {
|
||||
const { data } = await server.notifications.list({ token: userToken, start: 2, count: 3 })
|
||||
const ids = data.map(n => n.id)
|
||||
|
@ -227,7 +225,7 @@ describe('Test notifications API', function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
|
|
|
@ -35,7 +35,6 @@ describe('Test registrations notifications', function () {
|
|||
})
|
||||
|
||||
describe('New direct registration for moderators', function () {
|
||||
|
||||
before(async function () {
|
||||
await server.config.enableSignup(false)
|
||||
})
|
||||
|
@ -55,7 +54,6 @@ describe('Test registrations notifications', function () {
|
|||
})
|
||||
|
||||
describe('New registration request for moderators', function () {
|
||||
|
||||
before(async function () {
|
||||
await server.config.enableSignup(true)
|
||||
})
|
||||
|
@ -76,7 +74,7 @@ describe('Test registrations notifications', function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
|
|
|
@ -396,7 +396,6 @@ describe('Test user notifications', function () {
|
|||
})
|
||||
|
||||
describe('My live replay is published', function () {
|
||||
|
||||
let baseParams: CheckerBaseParams
|
||||
|
||||
before(() => {
|
||||
|
@ -640,7 +639,7 @@ describe('Test user notifications', function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
|
|
|
@ -32,6 +32,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
|
|||
expect(data.instance.hardwareInformation).to.be.empty
|
||||
expect(data.instance.serverCountry).to.be.empty
|
||||
expect(data.instance.support.text).to.be.empty
|
||||
expect(data.instance.defaultLanguage).to.equal('en')
|
||||
expect(data.instance.social.externalLink).to.be.empty
|
||||
expect(data.instance.social.blueskyLink).to.be.empty
|
||||
expect(data.instance.social.mastodonLink).to.be.empty
|
||||
|
@ -189,6 +190,7 @@ function buildNewCustomConfig (server: PeerTubeServer): CustomConfig {
|
|||
support: {
|
||||
text: 'My support text'
|
||||
},
|
||||
defaultLanguage: 'fr',
|
||||
social: {
|
||||
externalLink: 'https://joinpeertube.org/',
|
||||
mastodonLink: 'https://framapiaf.org/@peertube',
|
||||
|
@ -984,9 +986,9 @@ describe('Test config', function () {
|
|||
expect(body.short_name).to.equal(body.name)
|
||||
expect(body.description).to.equal('description manifest')
|
||||
|
||||
const icon = body.icons.find(f => f.sizes === '36x36')
|
||||
const icon = body.icons.find(f => f.sizes === '192x192')
|
||||
expect(icon).to.exist
|
||||
expect(icon.src).to.equal('/client/assets/images/icons/icon-36x36.png')
|
||||
expect(icon.src).to.equal('/client/assets/images/icons/icon-192x192.png')
|
||||
})
|
||||
|
||||
it('Should generate the manifest with avatar', async function () {
|
||||
|
|
|
@ -94,7 +94,7 @@ describe('Test contact form', function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, VideoCreateResult } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
ConfigCommand,
|
||||
|
@ -12,7 +12,9 @@ import {
|
|||
import { expectStartWith } from '@tests/shared/checks.js'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
|
||||
import { SQLCommand } from '@tests/shared/sql-command.js'
|
||||
import { expect } from 'chai'
|
||||
import { config, expect } from 'chai'
|
||||
|
||||
config.truncateThreshold = 0
|
||||
|
||||
describe('Test emails', function () {
|
||||
let server: PeerTubeServer
|
||||
|
@ -256,7 +258,7 @@ describe('Test emails', function () {
|
|||
expect(email['from'][0]['name']).equal('PeerTube')
|
||||
expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
|
||||
expect(email['to'][0]['address']).equal('user_1@example.com')
|
||||
expect(email['subject']).contains(' blacklisted')
|
||||
expect(email['subject']).contains(' blocked')
|
||||
expect(email['text']).contains('my super user video')
|
||||
expect(email['text']).contains('my super reason')
|
||||
})
|
||||
|
@ -272,7 +274,7 @@ describe('Test emails', function () {
|
|||
expect(email['from'][0]['name']).equal('PeerTube')
|
||||
expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
|
||||
expect(email['to'][0]['address']).equal('user_1@example.com')
|
||||
expect(email['subject']).contains(' unblacklisted')
|
||||
expect(email['subject']).contains(' unblocked')
|
||||
expect(email['text']).contains('my super user video')
|
||||
})
|
||||
|
||||
|
@ -412,8 +414,71 @@ describe('Test emails', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Email translations', function () {
|
||||
let video: VideoCreateResult
|
||||
|
||||
before(async function () {
|
||||
video = await server.videos.quickUpload({ name: 'video' })
|
||||
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
instance: {
|
||||
defaultLanguage: 'fr'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Should translate emails according to the instance language', async function () {
|
||||
await server.contactForm.send({
|
||||
fromEmail: 'toto@example.com',
|
||||
body: 'my super message',
|
||||
subject: 'my subject',
|
||||
fromName: 'Super toto'
|
||||
})
|
||||
|
||||
await waitJobs(server)
|
||||
|
||||
const email = emails[emails.length - 1]
|
||||
|
||||
expect(email['subject']).to.contain('Formulaire de contact')
|
||||
expect(email['text']).to.contain('Super toto vous a envoyé un message')
|
||||
})
|
||||
|
||||
it('Should translate emails according to the instance language if not provided by the user', async function () {
|
||||
await server.blacklist.add({ videoId: video.uuid })
|
||||
await waitJobs(server)
|
||||
|
||||
const email = emails[emails.length - 1]
|
||||
expect(email['subject']).to.contain('été bloquée')
|
||||
expect(email['text']).to.contain('été bloquée')
|
||||
})
|
||||
|
||||
it('Should translate emails according to the user language if provided', async function () {
|
||||
await server.users.updateMe({ language: 'en' })
|
||||
|
||||
await server.blacklist.remove({ videoId: video.uuid })
|
||||
await waitJobs(server)
|
||||
|
||||
const email = emails[emails.length - 1]
|
||||
expect(email['subject']).to.contain('has been unblocked')
|
||||
expect(email['text']).to.contain('has been unblocked')
|
||||
})
|
||||
|
||||
it('Should update user language and translate emails accordingly', async function () {
|
||||
await server.users.updateMe({ language: 'fr' })
|
||||
|
||||
await server.blacklist.add({ videoId: video.uuid })
|
||||
await waitJobs(server)
|
||||
|
||||
const email = emails[emails.length - 1]
|
||||
expect(email['subject']).to.contain('été bloquée')
|
||||
expect(email['text']).to.contain('été bloquée')
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
|
|
|
@ -27,6 +27,13 @@ describe('Test registrations', function () {
|
|||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await server.config.enableSignup(false)
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
instance: {
|
||||
defaultLanguage: 'fr'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Direct registrations of a new user', function () {
|
||||
|
@ -210,8 +217,8 @@ describe('Test registrations', function () {
|
|||
const email = emails.find(e => e['to'][0]['address'] === 'user4@example.com')
|
||||
expect(email).to.exist
|
||||
|
||||
expect(email['subject']).to.contain('been rejected')
|
||||
expect(email['text']).to.contain('been rejected')
|
||||
expect(email['subject']).to.contain('été rejetée')
|
||||
expect(email['text']).to.contain('été rejetée')
|
||||
expect(email['text']).to.contain('I do not want id 4 on this instance')
|
||||
})
|
||||
|
||||
|
@ -229,8 +236,8 @@ describe('Test registrations', function () {
|
|||
const email = emails.find(e => e['to'][0]['address'] === 'user2@example.com')
|
||||
expect(email).to.exist
|
||||
|
||||
expect(email['subject']).to.contain('been accepted')
|
||||
expect(email['text']).to.contain('been accepted')
|
||||
expect(email['subject']).to.contain('été acceptée')
|
||||
expect(email['text']).to.contain('été acceptée')
|
||||
expect(email['text']).to.contain('Welcome id 2')
|
||||
}
|
||||
|
||||
|
@ -238,8 +245,8 @@ describe('Test registrations', function () {
|
|||
const email = emails.find(e => e['to'][0]['address'] === 'user3@example.com')
|
||||
expect(email).to.exist
|
||||
|
||||
expect(email['subject']).to.contain('been accepted')
|
||||
expect(email['text']).to.contain('been accepted')
|
||||
expect(email['subject']).to.contain('été acceptée')
|
||||
expect(email['text']).to.contain('été acceptée')
|
||||
expect(email['text']).to.contain('Welcome id 3')
|
||||
}
|
||||
})
|
||||
|
@ -259,6 +266,7 @@ describe('Test registrations', function () {
|
|||
expect(me.videoChannels[0].displayName).to.equal('Main user2 channel')
|
||||
expect(me.role.id).to.equal(UserRole.USER)
|
||||
expect(me.email).to.equal('user2@example.com')
|
||||
expect(me.language).to.equal('fr')
|
||||
})
|
||||
|
||||
it('Should have created the appropriate attributes for user 3', async function () {
|
||||
|
@ -271,6 +279,7 @@ describe('Test registrations', function () {
|
|||
expect(me.videoChannels[0].displayName).to.equal('my user 3 channel')
|
||||
expect(me.role.id).to.equal(UserRole.USER)
|
||||
expect(me.email).to.equal('user3@example.com')
|
||||
expect(me.language).to.equal('fr')
|
||||
})
|
||||
|
||||
it('Should list these accepted/rejected registration requests', async function () {
|
||||
|
@ -408,7 +417,7 @@ describe('Test registrations', function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
|
|
|
@ -86,7 +86,6 @@ function runTest (withObjectStorage: boolean) {
|
|||
objectStorage = withObjectStorage
|
||||
? new ObjectStorageCommand()
|
||||
: undefined
|
||||
|
||||
;({
|
||||
rootId,
|
||||
noahId,
|
||||
|
@ -906,7 +905,7 @@ function runTest (withObjectStorage: boolean) {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server, remoteServer ])
|
||||
})
|
||||
|
|
|
@ -170,6 +170,7 @@ function runTest (withObjectStorage: boolean) {
|
|||
const me = await remoteServer.users.getMyInfo({ token: remoteNoahToken })
|
||||
|
||||
expect(me.p2pEnabled).to.be.false
|
||||
expect(me.language).to.equal('fr')
|
||||
|
||||
const settings = me.notificationSettings
|
||||
|
||||
|
@ -593,11 +594,11 @@ function runTest (withObjectStorage: boolean) {
|
|||
it('Should have received an email on finished import', async function () {
|
||||
const email = emails.reverse().find(e => {
|
||||
return e['to'][0]['address'] === 'noah_remote@example.com' &&
|
||||
e['subject'].includes('archive import has finished')
|
||||
e['subject'].includes('importation de votre archive est terminée')
|
||||
})
|
||||
|
||||
expect(email).to.exist
|
||||
expect(email['text']).to.contain('as considered duplicate: 5') // 5 videos are considered as duplicates
|
||||
expect(email['text']).to.contain('considéré comme doublon : 5') // 5 videos are considered as duplicates
|
||||
})
|
||||
|
||||
it('Should auto blacklist imported videos if enabled by the administrator', async function () {
|
||||
|
@ -715,7 +716,7 @@ function runTest (withObjectStorage: boolean) {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server, remoteServer, blockedServer ])
|
||||
})
|
||||
|
|
|
@ -158,7 +158,7 @@ describe('Test users email verification', function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { testAvatarSize } from '@tests/shared/checks.js'
|
||||
import { AbuseState, HttpStatusCode, UserAdminFlag, UserRole, VideoPlaylistType } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
|
||||
import { testAvatarSize } from '@tests/shared/checks.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test users', function () {
|
||||
let server: PeerTubeServer
|
||||
|
@ -60,6 +55,7 @@ describe('Test users', function () {
|
|||
expect(user.id).to.be.a('number')
|
||||
expect(user.account.displayName).to.equal('user_1')
|
||||
expect(user.account.description).to.be.null
|
||||
expect(user.language).to.equal('en')
|
||||
}
|
||||
|
||||
expect(userMe.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST)
|
||||
|
@ -334,6 +330,43 @@ describe('Test users', function () {
|
|||
expect(user.noInstanceConfigWarningModal).to.be.true
|
||||
expect(user.noAccountSetupWarningModal).to.be.true
|
||||
})
|
||||
|
||||
it('Should update instance config and automatically update user language', async function () {
|
||||
{
|
||||
const user = await server.users.getMyInfo({ token: userToken })
|
||||
expect(user.videoLanguages).to.be.null
|
||||
expect(user.language).to.equal('en')
|
||||
}
|
||||
|
||||
{
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
instance: {
|
||||
defaultLanguage: 'es'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const user = await server.users.getMyInfo({ token: userToken })
|
||||
expect(user.language).to.equal('es')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should be able to update my languages', async function () {
|
||||
await server.users.updateMe({
|
||||
token: userToken,
|
||||
language: 'fr',
|
||||
videoLanguages: [ 'fr', 'en' ]
|
||||
})
|
||||
|
||||
{
|
||||
const user = await server.users.getMyInfo({ token: userToken })
|
||||
expect(user.language).to.equal('fr')
|
||||
expect(user.videoLanguages).to.deep.equal([ 'fr', 'en' ])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Updating another user', function () {
|
||||
|
@ -527,6 +560,32 @@ describe('Test users', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Client config', function () {
|
||||
it('Send a cookie on interface language change', async function () {
|
||||
const res = await server.users.updateInterfaceLanguage({
|
||||
language: 'fr',
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
|
||||
const setCookie = res.headers['set-cookie']
|
||||
|
||||
expect(setCookie).to.exist
|
||||
expect(setCookie[0]).to.include('clientLanguage=fr;')
|
||||
})
|
||||
|
||||
it('Should clear cookies if language is null', async function () {
|
||||
const res = await server.users.updateInterfaceLanguage({
|
||||
language: null,
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
|
||||
const setCookie = res.headers['set-cookie']
|
||||
|
||||
expect(setCookie).to.exist
|
||||
expect(setCookie[0]).to.include('clientLanguage=;')
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
|
|
|
@ -956,7 +956,7 @@ describe('Test plugin filter hooks', function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
MockSmtpServer.Instance.kill()
|
||||
await MockSmtpServer.Instance.kill()
|
||||
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
|
|
|
@ -262,7 +262,7 @@ export async function prepareImportExportTests (options: {
|
|||
})
|
||||
|
||||
// My settings
|
||||
await server.users.updateMe({ token: noahToken, description: 'super noah description', p2pEnabled: false })
|
||||
await server.users.updateMe({ token: noahToken, description: 'super noah description', p2pEnabled: false, language: 'fr' })
|
||||
|
||||
// My notification settings
|
||||
await server.notifications.updateMySettings({
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
import MailDev from '@peertube/maildev'
|
||||
import { randomInt } from '@peertube/peertube-core-utils'
|
||||
import { parallelTests } from '@peertube/peertube-node-utils'
|
||||
import MailDev from 'maildev'
|
||||
|
||||
class MockSmtpServer {
|
||||
|
||||
private static instance: MockSmtpServer
|
||||
private started = false
|
||||
private maildev: any
|
||||
private emails: object[]
|
||||
private relayingEmail: Promise<void>
|
||||
|
||||
private constructor () { }
|
||||
private
|
||||
|
||||
private constructor () {}
|
||||
|
||||
collectEmails (emailsCollection: object[]) {
|
||||
const outgoingHost = process.env.MAILDEV_RELAY_HOST
|
||||
const outgoingPort = process.env.MAILDEV_RELAY_PORT
|
||||
? parseInt(process.env.MAILDEV_RELAY_PORT)
|
||||
: undefined
|
||||
|
||||
return new Promise<number>((res, rej) => {
|
||||
const port = parallelTests() ? randomInt(1025, 2000) : 1025
|
||||
this.emails = emailsCollection
|
||||
|
@ -24,11 +31,26 @@ class MockSmtpServer {
|
|||
ip: '127.0.0.1',
|
||||
smtp: port,
|
||||
disableWeb: true,
|
||||
silent: true
|
||||
silent: true,
|
||||
outgoingHost,
|
||||
outgoingPort
|
||||
})
|
||||
|
||||
this.maildev.on('new', email => {
|
||||
this.emails.push(email)
|
||||
|
||||
if (outgoingHost || outgoingPort) {
|
||||
this.relayingEmail = new Promise(resolve => {
|
||||
this.maildev.relayMail(email, function (err) {
|
||||
if (err) return console.log(err)
|
||||
|
||||
console.log('Email has been relayed!')
|
||||
|
||||
this.relayingEmail = undefined
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.maildev.listen(err => {
|
||||
|
@ -41,9 +63,13 @@ class MockSmtpServer {
|
|||
})
|
||||
}
|
||||
|
||||
kill () {
|
||||
async kill () {
|
||||
if (!this.maildev) return
|
||||
|
||||
if (this.relayingEmail) {
|
||||
await this.relayingEmail
|
||||
}
|
||||
|
||||
this.maildev.close()
|
||||
|
||||
this.maildev = null
|
||||
|
|
|
@ -77,11 +77,13 @@ async function waitUntilNotification (options: {
|
|||
await waitJobs([ server ])
|
||||
}
|
||||
|
||||
async function checkNewVideoFromSubscription (options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkNewVideoFromSubscription (
|
||||
options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { videoName, shortUUID } = options
|
||||
const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION
|
||||
|
||||
|
@ -107,11 +109,13 @@ async function checkNewVideoFromSubscription (options: CheckerBaseParams & {
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkNewLiveFromSubscription (options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkNewLiveFromSubscription (
|
||||
options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { videoName, shortUUID } = options
|
||||
const notificationType = UserNotificationType.NEW_LIVE_FROM_SUBSCRIPTION
|
||||
|
||||
|
@ -137,11 +141,13 @@ async function checkNewLiveFromSubscription (options: CheckerBaseParams & {
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkMyVideoIsPublished (options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkMyVideoIsPublished (
|
||||
options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { videoName, shortUUID } = options
|
||||
const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED
|
||||
|
||||
|
@ -165,11 +171,13 @@ async function checkMyVideoIsPublished (options: CheckerBaseParams & {
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkVideoStudioEditionIsFinished (options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkVideoStudioEditionIsFinished (
|
||||
options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { videoName, shortUUID } = options
|
||||
const notificationType = UserNotificationType.MY_VIDEO_STUDIO_EDITION_FINISHED
|
||||
|
||||
|
@ -193,13 +201,15 @@ async function checkVideoStudioEditionIsFinished (options: CheckerBaseParams & {
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkMyVideoImportIsFinished (options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
url: string
|
||||
success: boolean
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkMyVideoImportIsFinished (
|
||||
options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
url: string
|
||||
success: boolean
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { videoName, shortUUID, url, success } = options
|
||||
|
||||
const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR
|
||||
|
@ -219,9 +229,11 @@ async function checkMyVideoImportIsFinished (options: CheckerBaseParams & {
|
|||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
const toFind = success ? ' finished' : ' error'
|
||||
const toFind = success
|
||||
? /\bfinished\b/
|
||||
: /\berror\b/
|
||||
|
||||
return text.includes(url) && text.includes(toFind)
|
||||
return text.includes(url) && !!text.match(toFind)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
|
@ -229,10 +241,12 @@ async function checkMyVideoImportIsFinished (options: CheckerBaseParams & {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkUserRegistered (options: CheckerBaseParams & {
|
||||
username: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkUserRegistered (
|
||||
options: CheckerBaseParams & {
|
||||
username: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { username } = options
|
||||
const notificationType = UserNotificationType.NEW_USER_REGISTRATION
|
||||
|
||||
|
@ -257,11 +271,13 @@ async function checkUserRegistered (options: CheckerBaseParams & {
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkRegistrationRequest (options: CheckerBaseParams & {
|
||||
username: string
|
||||
registrationReason: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkRegistrationRequest (
|
||||
options: CheckerBaseParams & {
|
||||
username: string
|
||||
registrationReason: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { username, registrationReason } = options
|
||||
const notificationType = UserNotificationType.NEW_USER_REGISTRATION_REQUEST
|
||||
|
||||
|
@ -287,13 +303,15 @@ async function checkRegistrationRequest (options: CheckerBaseParams & {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkNewActorFollow (options: CheckerBaseParams & {
|
||||
followType: 'channel' | 'account'
|
||||
followerName: string
|
||||
followerDisplayName: string
|
||||
followingDisplayName: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkNewActorFollow (
|
||||
options: CheckerBaseParams & {
|
||||
followType: 'channel' | 'account'
|
||||
followerName: string
|
||||
followerDisplayName: string
|
||||
followingDisplayName: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { followType, followerName, followerDisplayName, followingDisplayName } = options
|
||||
const notificationType = UserNotificationType.NEW_FOLLOW
|
||||
|
||||
|
@ -327,10 +345,12 @@ async function checkNewActorFollow (options: CheckerBaseParams & {
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkNewInstanceFollower (options: CheckerBaseParams & {
|
||||
followerHost: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkNewInstanceFollower (
|
||||
options: CheckerBaseParams & {
|
||||
followerHost: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { followerHost } = options
|
||||
const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER
|
||||
|
||||
|
@ -354,17 +374,19 @@ async function checkNewInstanceFollower (options: CheckerBaseParams & {
|
|||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
|
||||
return text.includes('instance has a new follower') && text.includes(followerHost)
|
||||
return text.includes('PeerTube has a new follower') && text.includes(followerHost)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkAutoInstanceFollowing (options: CheckerBaseParams & {
|
||||
followerHost: string
|
||||
followingHost: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkAutoInstanceFollowing (
|
||||
options: CheckerBaseParams & {
|
||||
followerHost: string
|
||||
followingHost: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { followerHost, followingHost } = options
|
||||
const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING
|
||||
|
||||
|
@ -391,19 +413,21 @@ async function checkAutoInstanceFollowing (options: CheckerBaseParams & {
|
|||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
|
||||
return text.includes(' automatically followed a new instance') && text.includes(followingHost)
|
||||
return text.match(/\bautomatically followed\b/) && text.includes(followingHost)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkCommentMention (options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
commentId: number
|
||||
threadId: number
|
||||
byAccountDisplayName: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkCommentMention (
|
||||
options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
commentId: number
|
||||
threadId: number
|
||||
byAccountDisplayName: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { shortUUID, commentId, threadId, byAccountDisplayName } = options
|
||||
const notificationType = UserNotificationType.COMMENT_MENTION
|
||||
|
||||
|
@ -425,7 +449,7 @@ async function checkCommentMention (options: CheckerBaseParams & {
|
|||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
|
||||
return text.includes(' mentioned ') && text.includes(shortUUID) && text.includes(byAccountDisplayName)
|
||||
return text.match(/\bmentioned\b/) && text.includes(shortUUID) && text.includes(byAccountDisplayName)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
|
@ -433,13 +457,15 @@ async function checkCommentMention (options: CheckerBaseParams & {
|
|||
|
||||
let lastEmailCount = 0
|
||||
|
||||
async function checkNewCommentOnMyVideo (options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
commentId: number
|
||||
threadId: number
|
||||
checkType: CheckerType
|
||||
approval?: boolean // default false
|
||||
}) {
|
||||
async function checkNewCommentOnMyVideo (
|
||||
options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
commentId: number
|
||||
threadId: number
|
||||
checkType: CheckerType
|
||||
approval?: boolean // default false
|
||||
}
|
||||
) {
|
||||
const { server, shortUUID, commentId, threadId, checkType, emails, approval = false } = options
|
||||
const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
|
||||
|
||||
|
@ -468,7 +494,7 @@ async function checkNewCommentOnMyVideo (options: CheckerBaseParams & {
|
|||
const text = email['text']
|
||||
|
||||
return text.includes(commentUrl) &&
|
||||
(approval && text.includes('requires approval')) ||
|
||||
(approval && text.includes('requires approval')) ||
|
||||
(!approval && !text.includes('requires approval'))
|
||||
}
|
||||
|
||||
|
@ -481,11 +507,13 @@ async function checkNewCommentOnMyVideo (options: CheckerBaseParams & {
|
|||
}
|
||||
}
|
||||
|
||||
async function checkNewVideoAbuseForModerators (options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
videoName: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkNewVideoAbuseForModerators (
|
||||
options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
videoName: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { shortUUID, videoName } = options
|
||||
const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
|
||||
|
||||
|
@ -511,12 +539,14 @@ async function checkNewVideoAbuseForModerators (options: CheckerBaseParams & {
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkNewAbuseMessage (options: CheckerBaseParams & {
|
||||
abuseId: number
|
||||
message: string
|
||||
toEmail: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkNewAbuseMessage (
|
||||
options: CheckerBaseParams & {
|
||||
abuseId: number
|
||||
message: string
|
||||
toEmail: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { abuseId, message, toEmail } = options
|
||||
const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE
|
||||
|
||||
|
@ -543,11 +573,13 @@ async function checkNewAbuseMessage (options: CheckerBaseParams & {
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkAbuseStateChange (options: CheckerBaseParams & {
|
||||
abuseId: number
|
||||
state: AbuseStateType
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkAbuseStateChange (
|
||||
options: CheckerBaseParams & {
|
||||
abuseId: number
|
||||
state: AbuseStateType
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { abuseId, state } = options
|
||||
const notificationType = UserNotificationType.ABUSE_STATE_CHANGE
|
||||
|
||||
|
@ -578,11 +610,13 @@ async function checkAbuseStateChange (options: CheckerBaseParams & {
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkNewCommentAbuseForModerators (options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
videoName: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkNewCommentAbuseForModerators (
|
||||
options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
videoName: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { shortUUID, videoName } = options
|
||||
const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
|
||||
|
||||
|
@ -608,10 +642,12 @@ async function checkNewCommentAbuseForModerators (options: CheckerBaseParams & {
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkNewAccountAbuseForModerators (options: CheckerBaseParams & {
|
||||
displayName: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkNewAccountAbuseForModerators (
|
||||
options: CheckerBaseParams & {
|
||||
displayName: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { displayName } = options
|
||||
const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
|
||||
|
||||
|
@ -637,11 +673,13 @@ async function checkNewAccountAbuseForModerators (options: CheckerBaseParams & {
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkVideoAutoBlacklistForModerators (options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
videoName: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkVideoAutoBlacklistForModerators (
|
||||
options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
videoName: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { shortUUID, videoName } = options
|
||||
const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
|
||||
|
||||
|
@ -667,11 +705,13 @@ async function checkVideoAutoBlacklistForModerators (options: CheckerBaseParams
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkNewBlacklistOnMyVideo (options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
videoName: string
|
||||
blacklistType: 'blacklist' | 'unblacklist'
|
||||
}) {
|
||||
async function checkNewBlacklistOnMyVideo (
|
||||
options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
videoName: string
|
||||
blacklistType: 'blacklist' | 'unblacklist'
|
||||
}
|
||||
) {
|
||||
const { videoName, shortUUID, blacklistType } = options
|
||||
const notificationType = blacklistType === 'blacklist'
|
||||
? UserNotificationType.BLACKLIST_ON_MY_VIDEO
|
||||
|
@ -687,21 +727,24 @@ async function checkNewBlacklistOnMyVideo (options: CheckerBaseParams & {
|
|||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text = email['text']
|
||||
const blacklistText = blacklistType === 'blacklist'
|
||||
? 'blacklisted'
|
||||
: 'unblacklisted'
|
||||
const text: string = email['text']
|
||||
|
||||
return text.includes(shortUUID) && text.includes(blacklistText)
|
||||
const blacklistReg = blacklistType === 'blacklist'
|
||||
? /\bblocked\b/
|
||||
: /\bunblocked\b/
|
||||
|
||||
return text.includes(shortUUID) && !!text.match(blacklistReg)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder, checkType: 'presence' })
|
||||
}
|
||||
|
||||
async function checkNewPeerTubeVersion (options: CheckerBaseParams & {
|
||||
latestVersion: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkNewPeerTubeVersion (
|
||||
options: CheckerBaseParams & {
|
||||
latestVersion: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { latestVersion } = options
|
||||
const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION
|
||||
|
||||
|
@ -728,11 +771,13 @@ async function checkNewPeerTubeVersion (options: CheckerBaseParams & {
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkNewPluginVersion (options: CheckerBaseParams & {
|
||||
pluginType: PluginType_Type
|
||||
pluginName: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkNewPluginVersion (
|
||||
options: CheckerBaseParams & {
|
||||
pluginType: PluginType_Type
|
||||
pluginName: string
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { pluginName, pluginType } = options
|
||||
const notificationType = UserNotificationType.NEW_PLUGIN_VERSION
|
||||
|
||||
|
@ -759,15 +804,17 @@ async function checkNewPluginVersion (options: CheckerBaseParams & {
|
|||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkMyVideoTranscriptionGenerated (options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
language: {
|
||||
id: string
|
||||
label: string
|
||||
async function checkMyVideoTranscriptionGenerated (
|
||||
options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
language: {
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
checkType: CheckerType
|
||||
}
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
) {
|
||||
const { videoName, shortUUID, language } = options
|
||||
const notificationType = UserNotificationType.MY_VIDEO_TRANSCRIPTION_GENERATED
|
||||
|
||||
|
@ -872,11 +919,8 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an
|
|||
export {
|
||||
type CheckerType,
|
||||
type CheckerBaseParams,
|
||||
|
||||
getAllNotificationsSettings,
|
||||
|
||||
waitUntilNotification,
|
||||
|
||||
checkMyVideoImportIsFinished,
|
||||
checkUserRegistered,
|
||||
checkAutoInstanceFollowing,
|
||||
|
@ -904,11 +948,13 @@ export {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkNotification (options: CheckerBaseParams & {
|
||||
notificationChecker: (notification: UserNotification, checkType: CheckerType) => void
|
||||
emailNotificationFinder: (email: object) => boolean
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
async function checkNotification (
|
||||
options: CheckerBaseParams & {
|
||||
notificationChecker: (notification: UserNotification, checkType: CheckerType) => void
|
||||
emailNotificationFinder: (email: object) => boolean
|
||||
checkType: CheckerType
|
||||
}
|
||||
) {
|
||||
const { server, token, checkType, notificationChecker, emailNotificationFinder, socketNotifications, emails } = options
|
||||
|
||||
const check = options.check || { web: true, mail: true }
|
||||
|
|
|
@ -10,6 +10,7 @@ npm run tsc -- -b --verbose server/tsconfig.json
|
|||
npm run resolve-tspaths:server
|
||||
|
||||
cp -r "./server/core/static" "./server/core/assets" ./dist/core
|
||||
cp -r "./server/locales" ./dist
|
||||
cp "./server/scripts/upgrade.sh" "./dist/scripts"
|
||||
|
||||
mkdir -p ./client/dist && cp -r ./client/src/assets ./client/dist
|
||||
|
|
|
@ -19,6 +19,7 @@ mkdir -p "./dist/core/lib"
|
|||
npm run tsc -- -b -v --incremental server/tsconfig.json
|
||||
npm run resolve-tspaths:server
|
||||
|
||||
cp -r ./server/core/static ./server/core/assets ./dist/core
|
||||
cp -r "./server/core/static" "./server/core/assets" ./dist/core
|
||||
cp -r "./server/locales" ./dist
|
||||
|
||||
./node_modules/.bin/tsc-watch --build --preserveWatchOutput --verbose --onSuccess 'sh -c "npm run resolve-tspaths:server && NODE_ENV=dev node dist/server"' server/tsconfig.json
|
||||
|
|
|
@ -25,3 +25,7 @@ node ./node_modules/.bin/xliffmerge -p ./.xliffmerge.json $locales
|
|||
# Add our strings too
|
||||
cd ../
|
||||
npm run i18n:create-custom-files
|
||||
|
||||
# Generate server translations
|
||||
node ./node_modules/.bin/i18next -c server/.i18next-parser.config.ts server/core/**/*.{ts,hbs}
|
||||
|
||||
|
|
145
server/.i18next-parser.config.ts
Normal file
145
server/.i18next-parser.config.ts
Normal file
|
@ -0,0 +1,145 @@
|
|||
import { I18N_LOCALES } from '../packages/core-utils/dist/i18n/i18n.js'
|
||||
import { UserConfig } from 'i18next-parser'
|
||||
|
||||
export default {
|
||||
contextSeparator: '_',
|
||||
// Key separator used in your translation keys
|
||||
|
||||
createOldCatalogs: false,
|
||||
// Save the \_old files
|
||||
|
||||
defaultNamespace: 'translation',
|
||||
// Default namespace used in your i18next config
|
||||
|
||||
defaultValue: (_locale, _namespace, key, _value) => {
|
||||
return key as string
|
||||
},
|
||||
// Default value to give to keys with no value
|
||||
// You may also specify a function accepting the locale, namespace, key, and value as arguments
|
||||
|
||||
indentation: 2,
|
||||
// Indentation of the catalog files
|
||||
|
||||
keepRemoved: false,
|
||||
// Keep keys from the catalog that are no longer in code
|
||||
// You may either specify a boolean to keep or discard all removed keys.
|
||||
// You may also specify an array of patterns: the keys from the catalog that are no long in the code but match one of the patterns will be kept.
|
||||
// The patterns are applied to the full key including the namespace, the parent keys and the separators.
|
||||
|
||||
keySeparator: false,
|
||||
// Key separator used in your translation keys
|
||||
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
|
||||
|
||||
// see below for more details
|
||||
lexers: {
|
||||
hbs: [
|
||||
{
|
||||
lexer: 'HandlebarsLexer',
|
||||
functions: [ 't' ]
|
||||
}
|
||||
],
|
||||
handlebars: [ 'HandlebarsLexer' ],
|
||||
|
||||
htm: [ 'HTMLLexer' ],
|
||||
html: [ 'HTMLLexer' ],
|
||||
|
||||
mjs: [ 'JavascriptLexer' ],
|
||||
js: [ 'JavascriptLexer' ], // if you're writing jsx inside .js files, change this to JsxLexer
|
||||
ts: [
|
||||
{
|
||||
lexer: 'JavascriptLexer',
|
||||
functions: [ 't', 'tu' ]
|
||||
}
|
||||
],
|
||||
jsx: [ 'JsxLexer' ],
|
||||
tsx: [ 'JsxLexer' ],
|
||||
|
||||
default: [ 'JavascriptLexer' ]
|
||||
},
|
||||
|
||||
lineEnding: 'auto',
|
||||
// Control the line ending. See options at https://github.com/ryanve/eol
|
||||
|
||||
locales: Object.keys(I18N_LOCALES),
|
||||
// An array of the locales in your applications
|
||||
|
||||
namespaceSeparator: false,
|
||||
// Namespace separator used in your translation keys
|
||||
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
|
||||
|
||||
output: 'server/locales/$LOCALE/$NAMESPACE.json',
|
||||
// Supports $LOCALE and $NAMESPACE injection
|
||||
// Supports JSON (.json) and YAML (.yml) file formats
|
||||
// Where to write the locale files relative to process.cwd()
|
||||
|
||||
pluralSeparator: '_',
|
||||
// Plural separator used in your translation keys
|
||||
// If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys.
|
||||
// If you don't want to generate keys for plurals (for example, in case you are using ICU format), set `pluralSeparator: false`.
|
||||
|
||||
input: undefined,
|
||||
// An array of globs that describe where to look for source files
|
||||
// relative to the location of the configuration file
|
||||
|
||||
sort: false,
|
||||
// Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters)
|
||||
|
||||
verbose: false,
|
||||
// Display info about the parsing including some stats
|
||||
|
||||
failOnWarnings: false,
|
||||
// Exit with an exit code of 1 on warnings
|
||||
|
||||
failOnUpdate: false,
|
||||
// Exit with an exit code of 1 when translations are updated (for CI purpose)
|
||||
|
||||
customValueTemplate: null,
|
||||
// If you wish to customize the value output the value as an object, you can set your own format.
|
||||
//
|
||||
// - ${defaultValue} is the default value you set in your translation function.
|
||||
// - ${filePaths} will be expanded to an array that contains the absolute
|
||||
// file paths where the translations originated in, in case e.g., you need
|
||||
// to provide translators with context
|
||||
//
|
||||
// Any other custom property will be automatically extracted from the 2nd
|
||||
// argument of your `t()` function or tOptions in <Trans tOptions={...} />
|
||||
//
|
||||
// Example:
|
||||
// For `t('my-key', {maxLength: 150, defaultValue: 'Hello'})` in
|
||||
// /path/to/your/file.js,
|
||||
//
|
||||
// Using the following customValueTemplate:
|
||||
//
|
||||
// customValueTemplate: {
|
||||
// message: "${defaultValue}",
|
||||
// description: "${maxLength}",
|
||||
// paths: "${filePaths}",
|
||||
// }
|
||||
//
|
||||
// Will result in the following item being extracted:
|
||||
//
|
||||
// "my-key": {
|
||||
// "message": "Hello",
|
||||
// "description": 150,
|
||||
// "paths": ["/path/to/your/file.js"]
|
||||
// }
|
||||
|
||||
resetDefaultValueLocale: null,
|
||||
// The locale to compare with default values to determine whether a default value has been changed.
|
||||
// If this is set and a default value differs from a translation in the specified locale, all entries
|
||||
// for that key across locales are reset to the default value, and existing translations are moved to
|
||||
// the `_old` file.
|
||||
|
||||
i18nextOptions: null,
|
||||
// If you wish to customize options in internally used i18next instance, you can define an object with any
|
||||
// configuration property supported by i18next (https://www.i18next.com/overview/configuration-options).
|
||||
// { compatibilityJSON: 'v3' } can be used to generate v3 compatible plurals.
|
||||
|
||||
yamlOptions: null
|
||||
// If you wish to customize options for yaml output, you can define an object here.
|
||||
// Configuration options are here (https://github.com/nodeca/js-yaml#dump-object---options-).
|
||||
// Example:
|
||||
// {
|
||||
// lineWidth: -1,
|
||||
// }
|
||||
} satisfies UserConfig
|
|
@ -0,0 +1,10 @@
|
|||
{{! New message on abuse report }}
|
||||
{{#> base title=(t "New message on abuse report")}}
|
||||
<p>
|
||||
{{{t "A new message by {messageAccountName} was posted on <a href=\"{abuseUrl}\">abuse report #{abuseId}</a>" messageAccountName=messageAccountName abuseUrl=abuseUrl abuseId=abuseId}}}
|
||||
</p>
|
||||
|
||||
<blockquote>{{messageText}}</blockquote>
|
||||
|
||||
<br style="display: none;">
|
||||
{{/base}}
|
|
@ -1,11 +0,0 @@
|
|||
extends ../common/greetings
|
||||
include ../common/mixins.pug
|
||||
|
||||
block title
|
||||
| New message on abuse report
|
||||
|
||||
block content
|
||||
p
|
||||
| A new message by #{messageAccountName} was posted on #[a(href=abuseUrl) abuse report ##{abuseId}] on #{instanceName}
|
||||
blockquote #{messageText}
|
||||
br(style="display: none;")
|
|
@ -0,0 +1,10 @@
|
|||
{{! Abuse report state changed }}
|
||||
{{#> base title=(t "Abuse report state changed")}}
|
||||
<p>
|
||||
{{#if isAccepted}}
|
||||
{{{t "<a href=\"{abuseUrl}\">Your abuse report #{abuseId}</a> on {instanceName} has been accepted." abuseUrl=abuseUrl abuseId=abuseId instanceName=instanceName}}}
|
||||
{{else}}
|
||||
{{{t "<a href=\"{abuseUrl}\">Your abuse report #{abuseId}</a> on {instanceName} has been rejected." abuseUrl=abuseUrl abuseId=abuseId instanceName=instanceName}}}
|
||||
{{/if}}
|
||||
</p>
|
||||
{{/base}}
|
|
@ -1,9 +0,0 @@
|
|||
extends ../common/greetings
|
||||
include ../common/mixins.pug
|
||||
|
||||
block title
|
||||
| Abuse report state changed
|
||||
|
||||
block content
|
||||
p
|
||||
| #[a(href=abuseUrl) Your abuse report ##{abuseId}] on #{instanceName} has been #{isAccepted ? 'accepted' : 'rejected'}
|
|
@ -0,0 +1,18 @@
|
|||
{{! An account is pending moderation }}
|
||||
{{#> base title=(t "An account is pending moderation")}}
|
||||
<p>
|
||||
{{#if isLocal}}
|
||||
{{{t "{instanceName} received an abuse report for the account: <a href=\"{accountUrl}\">{accountDisplayName}</a>" accountUrl=accountUrl accountDisplayName=accountDisplayName instanceName=instanceName}}}
|
||||
{{else}}
|
||||
{{{t "{instanceName} received an abuse report for the remote account: <a href=\"{accountUrl}\">{accountDisplayName}</a>" accountUrl=accountUrl accountDisplayName=accountDisplayName instanceName=instanceName}}}
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{{t "The reporter, {reporter}, cited the following reason(s):" reporter=reporter}}
|
||||
</p>
|
||||
|
||||
<blockquote>{{reason}}</blockquote>
|
||||
|
||||
<br style="display: none;">
|
||||
{{/base}}
|
|
@ -1,14 +0,0 @@
|
|||
extends ../common/greetings
|
||||
include ../common/mixins.pug
|
||||
|
||||
block title
|
||||
| An account is pending moderation
|
||||
|
||||
block content
|
||||
p
|
||||
| #[a(href=WEBSERVER.URL) #{instanceName}] received an abuse report for the #{isLocal ? '' : 'remote '}account
|
||||
a(href=accountUrl) #{accountDisplayName}
|
||||
|
||||
p The reporter, #{reporter}, cited the following reason(s):
|
||||
blockquote #{reason}
|
||||
br(style="display: none;")
|
|
@ -1,219 +0,0 @@
|
|||
//-
|
||||
The email background color is defined in three places:
|
||||
1. body tag: for most email clients
|
||||
2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr
|
||||
3. mso conditional: For Windows 10 Mail
|
||||
doctype html
|
||||
head
|
||||
// This template is heavily adapted from the Cerberus Fluid template. Kudos to them!
|
||||
meta(charset='utf-8')
|
||||
//- utf-8 works for most cases
|
||||
meta(name='viewport' content='width=device-width')
|
||||
//- Forcing initial-scale shouldn't be necessary
|
||||
meta(http-equiv='X-UA-Compatible' content='IE=edge')
|
||||
//- Use the latest (edge) version of IE rendering engine
|
||||
meta(name='x-apple-disable-message-reformatting')
|
||||
//- Disable auto-scale in iOS 10 Mail entirely
|
||||
meta(name='format-detection' content='telephone=no,address=no,email=no,date=no,url=no')
|
||||
//- Tell iOS not to automatically link certain text strings.
|
||||
meta(name='color-scheme' content='light')
|
||||
meta(name='supported-color-schemes' content='light')
|
||||
//- The title tag shows in email notifications, like Android 4.4.
|
||||
title #{subject}
|
||||
//- What it does: Makes background images in 72ppi Outlook render at correct size.
|
||||
//if gte mso 9
|
||||
xml
|
||||
o:officedocumentsettings
|
||||
o:allowpng
|
||||
o:pixelsperinch 96
|
||||
//- CSS Reset : BEGIN
|
||||
style.
|
||||
/* What it does: Tells the email client that only light styles are provided but the client can transform them to dark. A duplicate of meta color-scheme meta tag above. */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
supported-color-schemes: light;
|
||||
}
|
||||
/* What it does: Remove spaces around the email design added by some email clients. */
|
||||
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
|
||||
html,
|
||||
body {
|
||||
margin: 0 auto !important;
|
||||
padding: 0 !important;
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
/* What it does: Stops email clients resizing small text. */
|
||||
* {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
/* What it does: Centers email on Android 4.4 */
|
||||
div[style*="margin: 16px 0"] {
|
||||
margin: 0 !important;
|
||||
}
|
||||
/* What it does: forces Samsung Android mail clients to use the entire viewport */
|
||||
#MessageViewBody, #MessageWebViewDiv{
|
||||
width: 100% !important;
|
||||
}
|
||||
/* What it does: Stops Outlook from adding extra spacing to tables. */
|
||||
table,
|
||||
td {
|
||||
mso-table-lspace: 0pt !important;
|
||||
mso-table-rspace: 0pt !important;
|
||||
}
|
||||
/* What it does: Fixes webkit padding issue. */
|
||||
table {
|
||||
border-spacing: 0 !important;
|
||||
border-collapse: collapse !important;
|
||||
table-layout: fixed !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
/* What it does: Uses a better rendering method when resizing images in IE. */
|
||||
img {
|
||||
-ms-interpolation-mode:bicubic;
|
||||
}
|
||||
a {
|
||||
color: #{fg};
|
||||
}
|
||||
a:not(.no-color) {
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: #{primary};
|
||||
text-underline-offset: 0.25em;
|
||||
text-decoration-thickness: 0.15em;
|
||||
}
|
||||
/* What it does: A work-around for email clients meddling in triggered links. */
|
||||
a[x-apple-data-detectors], /* iOS */
|
||||
.unstyle-auto-detected-links a,
|
||||
.aBn {
|
||||
border-bottom: 0 !important;
|
||||
cursor: default !important;
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
/* What it does: Prevents Gmail from displaying a download button on large, non-linked images. */
|
||||
.a6S {
|
||||
display: none !important;
|
||||
opacity: 0.01 !important;
|
||||
}
|
||||
/* What it does: Prevents Gmail from changing the text color in conversation threads. */
|
||||
.im {
|
||||
color: inherit !important;
|
||||
}
|
||||
/* If the above doesn't work, add a .g-img class to any image in question. */
|
||||
img.g-img + div {
|
||||
display: none !important;
|
||||
}
|
||||
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
|
||||
/* Create one of these media queries for each additional viewport size you'd like to fix */
|
||||
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
|
||||
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
|
||||
u ~ div .email-container {
|
||||
min-width: 320px !important;
|
||||
}
|
||||
}
|
||||
/* iPhone 6, 6S, 7, 8, and X */
|
||||
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
|
||||
u ~ div .email-container {
|
||||
min-width: 375px !important;
|
||||
}
|
||||
}
|
||||
/* iPhone 6+, 7+, and 8+ */
|
||||
@media only screen and (min-device-width: 414px) {
|
||||
u ~ div .email-container {
|
||||
min-width: 414px !important;
|
||||
}
|
||||
}
|
||||
//- CSS Reset : END
|
||||
//- CSS for PeerTube : START
|
||||
style.
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 10px;
|
||||
border-left: 2px solid #{primary};
|
||||
}
|
||||
//- CSS for PeerTube : END
|
||||
|
||||
body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; color: #{fg}; background-color: #{bg};")
|
||||
center(role='article' aria-roledescription='email' lang='en' style='width: 100%; background-color: #{bg};')
|
||||
//if mso | IE
|
||||
table(role='presentation' border='0' cellpadding='0' cellspacing='0' width='100%' style='background-color: #fff;')
|
||||
tr
|
||||
td
|
||||
//- Visually Hidden Preheader Text : BEGIN
|
||||
div(style='max-height:0; overflow:hidden; mso-hide:all;' aria-hidden='true')
|
||||
block preheader
|
||||
//- Visually Hidden Preheader Text : END
|
||||
|
||||
//- Create white space after the desired preview text so email clients don’t pull other distracting text into the inbox preview. Extend as necessary.
|
||||
//- Preview Text Spacing Hack : BEGIN
|
||||
div(style='display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;')
|
||||
| ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
//- Preview Text Spacing Hack : END
|
||||
|
||||
//-
|
||||
Set the email width. Defined in two places:
|
||||
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 600px.
|
||||
2. MSO tags for Desktop Windows Outlook enforce a 600px width.
|
||||
.email-container(style='max-width: 600px; margin: 0 auto;')
|
||||
//if mso
|
||||
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='600')
|
||||
tr
|
||||
td
|
||||
//- Email Body : BEGIN
|
||||
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
|
||||
//- 1 Column Text + Button : BEGIN
|
||||
tr
|
||||
td
|
||||
table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
|
||||
tr
|
||||
td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 1.5')
|
||||
table(role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%")
|
||||
tr
|
||||
td(width="40px")
|
||||
img(src=`${WEBSERVER.URL}/client/assets/images/icons/icon-192x192.png` width="auto" height="30px" alt="" border="0" style="height: 30px; font-family: sans-serif; font-size: 15px; line-height: 15px;")
|
||||
td
|
||||
h1(style='margin: 10px 0 10px 0; font-family: sans-serif; font-size: 25px; line-height: 30px; font-weight: normal;')
|
||||
block title
|
||||
if title
|
||||
| #{title}
|
||||
else
|
||||
| Something requires your attention
|
||||
p(style='margin: 0;')
|
||||
block body
|
||||
if action
|
||||
tr
|
||||
td(style='padding: 0 20px;')
|
||||
//- Button : BEGIN
|
||||
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' style='margin: auto;')
|
||||
tr
|
||||
td(style=`border-radius: 4px; background: ${primary};`)
|
||||
a.no-color(href=action.url style=`background: ${primary}; font-family: sans-serif; font-size: 15px; line-height: 15px; text-decoration: none; padding: 13px 17px; display: block; border-radius: 4px; font-weight: bold;`) #{action.text}
|
||||
//- Button : END
|
||||
//- 1 Column Text + Button : END
|
||||
//- Clear Spacer : BEGIN
|
||||
tr
|
||||
td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;')
|
||||
br
|
||||
//- Clear Spacer : END
|
||||
//- Email Body : END
|
||||
//- Email Footer : BEGIN
|
||||
unless hideNotificationPreferencesLink
|
||||
table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
|
||||
tr
|
||||
td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; text-align: center;')
|
||||
webversion
|
||||
a.no-color(href=`${WEBSERVER.URL}/my-account/notifications` style='font-weight: bold;') View in your notifications
|
||||
br
|
||||
tr
|
||||
td(style='padding: 20px; padding-top: 10px; font-family: sans-serif; font-size: 12px; text-align: center;')
|
||||
unsubscribe
|
||||
a.no-color(href=`${WEBSERVER.URL}/my-account/settings#notifications`) Manage your notification preferences in your profile
|
||||
br
|
||||
//- Email Footer : END
|
||||
//if mso
|
||||
//if mso | IE
|
|
@ -1,11 +0,0 @@
|
|||
extends base
|
||||
|
||||
block body
|
||||
if username
|
||||
p Hi #{username},
|
||||
|
||||
block content
|
||||
|
||||
if signature
|
||||
p
|
||||
| #{signature}
|
5
server/core/assets/email-templates/common/html.hbs
Normal file
5
server/core/assets/email-templates/common/html.hbs
Normal file
|
@ -0,0 +1,5 @@
|
|||
{{#> base}}
|
||||
<p>
|
||||
{{text}}
|
||||
</p>
|
||||
{{/base}}
|
|
@ -1,4 +0,0 @@
|
|||
extends greetings
|
||||
|
||||
block content
|
||||
p !{text}
|
|
@ -1,7 +0,0 @@
|
|||
mixin channel(channel)
|
||||
- var handle = `${channel.name}@${channel.host}`
|
||||
| #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}]
|
||||
|
||||
mixin account(account)
|
||||
- var handle = `${account.name}@${account.host}`
|
||||
| #[a(href=`${WEBSERVER.URL}/accounts/${handle}` title=handle) #{account.displayName}]
|
11
server/core/assets/email-templates/contact-form/html.hbs
Normal file
11
server/core/assets/email-templates/contact-form/html.hbs
Normal file
|
@ -0,0 +1,11 @@
|
|||
{{#> base title=(t "Someone just used the contact form")}}
|
||||
<p>
|
||||
{{{t "{fromName} sent you a message via the contact form on <a href=\"{webserverUrl}\">{instanceName}</a>: " fromName=fromName webserverUrl=WEBSERVER.URL instanceName=instanceName}}}
|
||||
</p>
|
||||
|
||||
<blockquote style="white-space: pre-wrap">{{body}}</blockquote>
|
||||
|
||||
<p>
|
||||
{{{t "You can contact them at <a href=\"mailto:{fromEmail}\">{fromEmail}</a>, or simply reply to this email to get in touch." fromEmail=fromEmail}}}
|
||||
</p>
|
||||
{{/base}}
|
|
@ -1,9 +0,0 @@
|
|||
extends ../common/greetings
|
||||
|
||||
block title
|
||||
| Someone just used the contact form
|
||||
|
||||
block content
|
||||
p #{fromName} sent you a message via the contact form on #[a(href=WEBSERVER.URL) #{instanceName}]:
|
||||
blockquote(style='white-space: pre-wrap') #{body}
|
||||
p You can contact them at #[a(href=`mailto:${fromEmail}`) #{fromEmail}], or simply reply to this email to get in touch.
|
|
@ -0,0 +1,9 @@
|
|||
{{#> base title=(t "New follower on your channel")}}
|
||||
<p>
|
||||
{{#if accountFollowType}}
|
||||
{{{t "Your account <a href=\"{followingUrl}\">{followingName}</a> has a new subscriber: <a href=\"{followerUrl}\">{followerName}</a>." followingUrl=followingUrl followingName=followingName followerUrl=followerUrl followerName=followerName}}}
|
||||
{{else}}
|
||||
{{{t "Your channel <a href=\"{followingUrl}\">{followingName}</a> has a new subscriber: <a href=\"{followerUrl}\">{followerName}</a>." followingUrl=followingUrl followingName=followingName followerUrl=followerUrl followerName=followerName}}}
|
||||
{{/if}}
|
||||
</p>
|
||||
{{/base}}
|
|
@ -1,9 +0,0 @@
|
|||
extends ../common/greetings
|
||||
|
||||
block title
|
||||
| New follower on your channel
|
||||
|
||||
block content
|
||||
p.
|
||||
Your #{followType} #[a(href=followingUrl) #{followingName}] has a new subscriber:
|
||||
#[a(href=followerUrl) #{followerName}].
|
|
@ -0,0 +1,15 @@
|
|||
{{#> base title=(t "Your account has been blocked")}}
|
||||
<p>
|
||||
{{#if reason}}
|
||||
{{{t "Your account <strong>{username}</strong> has been blocked by {instanceName} moderators for the following reason:" username=username instanceName=instanceName}}}
|
||||
{{else}}
|
||||
{{{t "Your account <strong>{username}</strong> has been blocked by {instanceName} moderators." username=username instanceName=instanceName}}}
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
{{#if reason}}
|
||||
<blockquote>{{reason}}</blockquote>
|
||||
{{/if}}
|
||||
|
||||
<br style="display: none;">
|
||||
{{/base}}
|
|
@ -0,0 +1,5 @@
|
|||
{{#> base title=(t "Your account has been unblocked")}}
|
||||
<p>
|
||||
{{{t "Your account <strong>{username}</strong> has been unblocked by {instanceName} moderators." username=username instanceName=instanceName}}}
|
||||
</p>
|
||||
{{/base}}
|
268
server/core/assets/email-templates/partials/base.hbs
Normal file
268
server/core/assets/email-templates/partials/base.hbs
Normal file
|
@ -0,0 +1,268 @@
|
|||
{{!
|
||||
The email background color is defined in three places:
|
||||
1. body tag: for most email clients
|
||||
2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr
|
||||
3. mso conditional: For Windows 10 Mail
|
||||
}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- This template is heavily adapted from the Cerberus Fluid template. Kudos to them! -->
|
||||
<meta charset="utf-8">
|
||||
<!-- utf-8 works for most cases -->
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<!-- Forcing initial-scale shouldn't be necessary -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!-- Use the latest (edge) version of IE rendering engine -->
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<!-- Disable auto-scale in iOS 10 Mail entirely -->
|
||||
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
|
||||
<!-- Tell iOS not to automatically link certain text strings. -->
|
||||
<meta name="color-scheme" content="light">
|
||||
<meta name="supported-color-schemes" content="light">
|
||||
<!-- The title tag shows in email notifications, like Android 4.4. -->
|
||||
<title>{{subject}}</title>
|
||||
<!-- What it does: Makes background images in 72ppi Outlook render at correct size. -->
|
||||
<!--[if gte mso 9]>
|
||||
<xml>
|
||||
<o:officedocumentsettings>
|
||||
<o:allowpng>
|
||||
<o:pixelsperinch>96</o:pixelsperinch>
|
||||
</o:allowpng>
|
||||
</o:officedocumentsettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!-- CSS Reset : BEGIN -->
|
||||
<style>
|
||||
/* What it does: Tells the email client that only light styles are provided but the client can transform them to dark. A duplicate of meta color-scheme meta tag above. */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
supported-color-schemes: light;
|
||||
}
|
||||
/* What it does: Remove spaces around the email design added by some email clients. */
|
||||
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
|
||||
html,
|
||||
body {
|
||||
margin: 0 auto !important;
|
||||
padding: 0 !important;
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
/* What it does: Stops email clients resizing small text. */
|
||||
* {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
/* What it does: Centers email on Android 4.4 */
|
||||
div[style*="margin: 16px 0"] {
|
||||
margin: 0 !important;
|
||||
}
|
||||
/* What it does: forces Samsung Android mail clients to use the entire viewport */
|
||||
#MessageViewBody, #MessageWebViewDiv{
|
||||
width: 100% !important;
|
||||
}
|
||||
/* What it does: Stops Outlook from adding extra spacing to tables. */
|
||||
table,
|
||||
td {
|
||||
mso-table-lspace: 0pt !important;
|
||||
mso-table-rspace: 0pt !important;
|
||||
}
|
||||
/* What it does: Fixes webkit padding issue. */
|
||||
table {
|
||||
border-spacing: 0 !important;
|
||||
border-collapse: collapse !important;
|
||||
table-layout: fixed !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
/* What it does: Uses a better rendering method when resizing images in IE. */
|
||||
img {
|
||||
-ms-interpolation-mode:bicubic;
|
||||
}
|
||||
a {
|
||||
color: {{fg}};
|
||||
}
|
||||
a:not(.no-color) {
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: {{primary}};
|
||||
text-underline-offset: 0.25em;
|
||||
text-decoration-thickness: 0.15em;
|
||||
}
|
||||
/* What it does: A work-around for email clients meddling in triggered links. */
|
||||
a[x-apple-data-detectors], /* iOS */
|
||||
.unstyle-auto-detected-links a,
|
||||
.aBn {
|
||||
border-bottom: 0 !important;
|
||||
cursor: default !important;
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
/* What it does: Prevents Gmail from displaying a download button on large, non-linked images. */
|
||||
.a6S {
|
||||
display: none !important;
|
||||
opacity: 0.01 !important;
|
||||
}
|
||||
/* What it does: Prevents Gmail from changing the text color in conversation threads. */
|
||||
.im {
|
||||
color: inherit !important;
|
||||
}
|
||||
/* If the above doesn't work, add a .g-img class to any image in question. */
|
||||
img.g-img + div {
|
||||
display: none !important;
|
||||
}
|
||||
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
|
||||
/* Create one of these media queries for each additional viewport size you'd like to fix */
|
||||
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
|
||||
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
|
||||
u ~ div .email-container {
|
||||
min-width: 320px !important;
|
||||
}
|
||||
}
|
||||
/* iPhone 6, 6S, 7, 8, and X */
|
||||
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
|
||||
u ~ div .email-container {
|
||||
min-width: 375px !important;
|
||||
}
|
||||
}
|
||||
/* iPhone 6+, 7+, and 8+ */
|
||||
@media only screen and (min-device-width: 414px) {
|
||||
u ~ div .email-container {
|
||||
min-width: 414px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- CSS Reset : END -->
|
||||
<!-- CSS for PeerTube : START -->
|
||||
<style>
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 10px;
|
||||
border-left: 2px solid {{primary}};
|
||||
}
|
||||
</style>
|
||||
<!-- CSS for PeerTube : END -->
|
||||
</head>
|
||||
|
||||
<body width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; color: {{fg}}; background-color: {{bg}};">
|
||||
<center role="article" aria-roledescription="email" lang="en" style="width: 100%; background-color: {{bg}};">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #fff;">
|
||||
<tr>
|
||||
<td>
|
||||
<![endif]-->
|
||||
|
||||
<!-- Create white space after the desired preview text so email clients don't pull other distracting text into the inbox preview. Extend as necessary. -->
|
||||
<!-- Preview Text Spacing Hack : BEGIN -->
|
||||
<div style="display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;">
|
||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
|
||||
</div>
|
||||
<!-- Preview Text Spacing Hack : END -->
|
||||
|
||||
<!--
|
||||
Set the email width. Defined in two places:
|
||||
1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 600px.
|
||||
2. MSO tags for Desktop Windows Outlook enforce a 600px width.
|
||||
-->
|
||||
<div class="email-container" style="max-width: 600px; margin: 0 auto;">
|
||||
<!--[if mso]>
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="600">
|
||||
<tr>
|
||||
<td>
|
||||
<![endif]-->
|
||||
<!-- Email Body : BEGIN -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: auto;">
|
||||
<!-- 1 Column Text + Button : BEGIN -->
|
||||
<tr>
|
||||
<td>
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 1.5">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td width="40px">
|
||||
<img src="{{logoUrl}}" width="auto" height="30px" alt="" border="0" style="height: 30px; font-family: sans-serif; font-size: 15px; line-height: 15px;">
|
||||
</td>
|
||||
<td>
|
||||
<h1 style="margin: 10px 0 10px 0; font-family: sans-serif; font-size: 25px; line-height: 30px; font-weight: normal;">
|
||||
{{#if title}}
|
||||
{{title}}
|
||||
{{else}}
|
||||
{{subject}}
|
||||
{{/if}}
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="margin: 0;">
|
||||
{{#if username}}
|
||||
<p>{{t "Hi {username}," username=username}}</p>
|
||||
{{/if}}
|
||||
|
||||
{{> @partial-block}}
|
||||
|
||||
{{#if signature}}
|
||||
<p>{{signature}}</p>
|
||||
{{/if}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
{{#if action}}
|
||||
<tr>
|
||||
<td style="padding: 0 20px;">
|
||||
{{> button actionUrl=action.url actionText=action.text}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 1 Column Text + Button : END -->
|
||||
<!-- Clear Spacer : BEGIN -->
|
||||
<tr>
|
||||
<td aria-hidden="true" height="20" style="font-size: 0px; line-height: 0px;">
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Clear Spacer : END -->
|
||||
</table>
|
||||
<!-- Email Body : END -->
|
||||
<!-- Email Footer : BEGIN -->
|
||||
{{#unless hideNotificationPreferencesLink}}
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: auto;">
|
||||
<tr>
|
||||
<td style="padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; text-align: center;">
|
||||
<a class="no-color" href="{{WEBSERVER.URL}}/my-account/notifications" style="font-weight: bold;">
|
||||
{{t "View in your notifications" }}
|
||||
</a>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 20px; padding-top: 10px; font-family: sans-serif; font-size: 12px; text-align: center;">
|
||||
<a class="no-color" href="{{WEBSERVER.URL}}/my-account/settings#notifications">
|
||||
{{t "Manage your notification preferences in your profile"}}
|
||||
</a>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/unless}}
|
||||
<!-- Email Footer : END -->
|
||||
<!--[if mso]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
7
server/core/assets/email-templates/partials/button.hbs
Normal file
7
server/core/assets/email-templates/partials/button.hbs
Normal file
|
@ -0,0 +1,7 @@
|
|||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
|
||||
<tr>
|
||||
<td style="border-radius: 4px; background: {{primary}};">
|
||||
<a class="no-color" href="{{actionUrl}}" style="background: {{primary}}; font-family: sans-serif; font-size: 15px; line-height: 15px; text-decoration: none; padding: 13px 17px; display: block; border-radius: 4px; font-weight: bold;">{{actionText}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
11
server/core/assets/email-templates/password-create/html.hbs
Normal file
11
server/core/assets/email-templates/password-create/html.hbs
Normal file
|
@ -0,0 +1,11 @@
|
|||
{{#> base title=(t "Password creation for your account")}}
|
||||
<p>
|
||||
{{{t "Welcome to <a href=\"{webserverUrl}\">{instanceName}</a>!" webserverUrl=WEBSERVER.URL instanceName=instanceName}}}
|
||||
</p>
|
||||
|
||||
<p>{{t "Your username is: {username}." username=username}}</p>
|
||||
|
||||
<p>{{t "Please click on the link below to set your password (this link will expire within seven days):"}}</p>
|
||||
|
||||
{{> button actionUrl=createPasswordUrl actionText=(t "Create my password")}}
|
||||
{{/base}}
|
|
@ -1,10 +0,0 @@
|
|||
extends ../common/greetings
|
||||
|
||||
block title
|
||||
| Password creation for your account
|
||||
|
||||
block content
|
||||
p.
|
||||
Welcome to #[a(href=WEBSERVER.URL) #{instanceName}]. Your username is: #{username}.
|
||||
Please set your password by following #[a(href=createPasswordUrl) this link]: #[a(href=createPasswordUrl) #{createPasswordUrl}]
|
||||
(this link will expire within seven days).
|
15
server/core/assets/email-templates/password-reset/html.hbs
Normal file
15
server/core/assets/email-templates/password-reset/html.hbs
Normal file
|
@ -0,0 +1,15 @@
|
|||
{{#> base title=(t "Password reset for your account")}}
|
||||
<p>
|
||||
{{t "A reset password procedure for your account has been requested on {instanceName}." username=username instanceName=instanceName}}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{{t "Please click on the link below to reset it (the link will expire within 1 hour):"}}
|
||||
</p>
|
||||
|
||||
{{> button actionUrl=resetPasswordUrl actionText=(t "Reset my password")}}
|
||||
|
||||
<p>
|
||||
{{t "If you are not the person who initiated this request, please let us know by replying to this email."}}
|
||||
</p>
|
||||
{{/base}}
|
|
@ -1,12 +0,0 @@
|
|||
extends ../common/greetings
|
||||
|
||||
block title
|
||||
| Password reset for your account
|
||||
|
||||
block content
|
||||
p.
|
||||
A reset password procedure for your account #{username} has been requested on #[a(href=WEBSERVER.URL) #{instanceName}].
|
||||
Please follow #[a(href=resetPasswordUrl) this link] to reset it: #[a(href=resetPasswordUrl) #{resetPasswordUrl}]
|
||||
(the link will expire within 1 hour).
|
||||
p.
|
||||
If you are not the person who initiated this request, please ignore this email.
|
|
@ -0,0 +1,7 @@
|
|||
{{#> base title=(t "New PeerTube version available")}}
|
||||
<p>
|
||||
{{t "A new version of PeerTube is available: {latestVersion}." latestVersion=latestVersion}}
|
||||
|
||||
{{{t "You can check the latest news on <a href=\"https://joinpeertube.org/news\">JoinPeerTube</a>."}}}
|
||||
</p>
|
||||
{{/base}}
|
|
@ -1,9 +0,0 @@
|
|||
extends ../common/greetings
|
||||
|
||||
block title
|
||||
| New PeerTube version available
|
||||
|
||||
block content
|
||||
p
|
||||
| A new version of PeerTube is available: #{latestVersion}.
|
||||
| You can check the latest news on #[a(href="https://joinpeertube.org/news") JoinPeerTube].
|
|
@ -0,0 +1,14 @@
|
|||
{{! New plugin version available }}
|
||||
{{#> base title=(t "New plugin version available")}}
|
||||
<p>
|
||||
{{#if isPlugin}}
|
||||
{{t "A new version of the plugin {pluginName} is available: {latestVersion}." pluginName=pluginName latestVersion=latestVersion}}
|
||||
{{else}}
|
||||
{{t "A new version of the theme {pluginName} is available: {latestVersion}." pluginName=pluginName latestVersion=latestVersion}}
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{{{t "You might want to upgrade it on <a href=\"{pluginUrl}\">your admin interface</a>." pluginUrl=pluginUrl}}}
|
||||
</p>
|
||||
{{/base}}
|
|
@ -1,9 +0,0 @@
|
|||
extends ../common/greetings
|
||||
|
||||
block title
|
||||
| New plugin version available
|
||||
|
||||
block content
|
||||
p
|
||||
| A new version of the plugin/theme #{pluginName} is available: #{latestVersion}.
|
||||
| You might want to upgrade it on #[a(href=pluginUrl) the PeerTube admin interface].
|
|
@ -0,0 +1,10 @@
|
|||
{{! Your export archive has been created }}
|
||||
{{#> base title=(t "Your export archive has been created")}}
|
||||
<p>
|
||||
{{t "Your export archive has been created."}}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{{{t "You can download it in <a href=\"{exportsUrl}\">your account export page</a>." exportsUrl=exportsUrl}}}
|
||||
</p>
|
||||
{{/base}}
|
|
@ -1,9 +0,0 @@
|
|||
extends ../common/greetings
|
||||
include ../common/mixins.pug
|
||||
|
||||
block title
|
||||
| Your export archive has been created
|
||||
|
||||
block content
|
||||
p
|
||||
| Your export archive has been created. You can download it in #[a(href=exportsUrl) your account export page].
|
|
@ -0,0 +1,11 @@
|
|||
{{#> base title=(t "Failed to create your export archive")}}
|
||||
<p>
|
||||
{{t "We are sorry but the generation of your export archive has failed:"}}
|
||||
</p>
|
||||
|
||||
<blockquote>{{errorMessage}}</blockquote>
|
||||
|
||||
<p>
|
||||
{{t "Please contact your administrator if the problem occurs again."}}
|
||||
</p>
|
||||
{{/base}}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue