Translation infrastructure

This commit is contained in:
Eugene Pankov
2022-01-08 16:02:56 +01:00
parent 04010b58bb
commit 0814d44207
134 changed files with 8137 additions and 889 deletions

View File

@@ -17,12 +17,15 @@
"author": "Eugene Pankov",
"license": "MIT",
"devDependencies": {
"@ngx-translate/core": "^14.0.0",
"bootstrap": "^4.1.3",
"deepmerge": "^4.1.1",
"js-yaml": "^4.0.0",
"messageformat": "^2.3.0",
"mixpanel": "^0.13.0",
"ngx-filesize": "^2.0.16",
"ngx-perfect-scrollbar": "^10.1.0",
"ngx-translate-messageformat-compiler": "^4.11.0",
"readable-stream": "3.6.0",
"uuid": "^8.0.0"
},

View File

@@ -35,4 +35,5 @@ export { TabsService, NewTabParameters, TabComponentType } from '../services/tab
export { UpdaterService } from '../services/updater.service'
export { VaultService, Vault, VaultSecret, VaultFileSecret, VAULT_SECRET_TYPE_FILE, StoredVault, VaultSecretKey } from '../services/vault.service'
export { FileProvidersService } from '../services/fileProviders.service'
export { LocaleService, TranslateServiceWrapper as TranslateService } from '../services/locale.service'
export * from '../utils'

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Injectable } from '@angular/core'
import { TranslateService } from '@ngx-translate/core'
import { ToolbarButton, ToolbarButtonProvider } from './api/toolbarButtonProvider'
import { HostAppService, Platform } from './api/hostApp'
@@ -12,6 +13,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
constructor (
private hostApp: HostAppService,
private profilesService: ProfilesService,
private translate: TranslateService,
hotkeys: HotkeysService,
) {
super()
@@ -35,7 +37,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
icon: this.hostApp.platform === Platform.Web
? require('./icons/plus.svg')
: require('./icons/profiles.svg'),
title: 'Profiles and connections',
title: this.translate.instant('Profiles and connections'),
click: () => this.activate(),
},
...this.profilesService.getRecentProfiles().map(profile => ({

View File

@@ -2,5 +2,5 @@
input.form-control(type='text', #input, [(ngModel)]='value', (keyup.enter)='save()', autofocus)
.modal-footer
button.btn.btn-primary((click)='save()') Save
button.btn.btn-secondary((click)='close()') Cancel
button.btn.btn-primary((click)='save()', translate) Save
button.btn.btn-secondary((click)='close()', translate) Cancel

View File

@@ -1,7 +1,7 @@
.modal-body
.alert.alert-danger Tabby could not start with your plugins, so all third party plugins have been disabled in this session. The error was:
.alert.alert-danger(translate) Tabby could not start with your plugins, so all third party plugins have been disabled in this session. The error was:
pre {{error}}
.modal-footer
button.btn.btn-primary((click)='close()') Close
button.btn.btn-primary((click)='close()', translate) Close

View File

@@ -15,9 +15,9 @@ footer.d-flex.align-items-center
.btn-group.mr-auto
button.btn.btn-dark((click)='homeBase.openGitHub()')
i.fab.fa-github
span GitHub
span(translate) GitHub
button.btn.btn-dark((click)='homeBase.reportBug()')
i.fas.fa-bug
span Report a problem
span(translate) Report a problem
.form-control-static.selectable.no-drag Version: {{homeBase.appVersion}}
.form-control-static.selectable.no-drag(translate, [translateParams]='{version: homeBase.appVersion}') Version: {version}

View File

@@ -1,5 +1,5 @@
.d-flex.align-items-center
.dropdown-header File transfers
.dropdown-header(translate) File transfers
button.btn.btn-link.ml-auto((click)='removeAll(); $event.stopPropagation()') !{require('../icons/times.svg')}
.transfer(*ngFor='let transfer of transfers', (click)='showTransfer(transfer)')
.icon(*ngIf='isDownload(transfer)') !{require('../icons/download.svg')}

View File

@@ -1,4 +1,5 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'
import { TranslateService } from '@ngx-translate/core'
import { FileDownload, FileTransfer, PlatformService } from '../api/platform'
/** @hidden */
@@ -11,7 +12,10 @@ export class TransfersMenuComponent {
@Input() transfers: FileTransfer[]
@Output() transfersChange = new EventEmitter<FileTransfer[]>()
constructor (private platform: PlatformService) { }
constructor (
private platform: PlatformService,
private translate: TranslateService,
) { }
isDownload (transfer: FileTransfer): boolean {
return transfer instanceof FileDownload
@@ -40,8 +44,11 @@ export class TransfersMenuComponent {
if (this.transfers.some(x => !x.isComplete())) {
if ((await this.platform.showMessageBox({
type: 'warning',
message: 'There are active file transfers',
buttons: ['Abort all', 'Do not abort'],
message: this.translate.instant('There are active file transfers'),
buttons: [
this.translate.instant('Abort all'),
this.translate.instant('Do not abort'),
],
defaultId: 1,
cancelId: 1,
})).response === 1) {

View File

@@ -1,13 +1,18 @@
.modal-body
.d-flex.align-items-center.mb-3
h3.m-0 Vault is locked
h3.m-0(translate) Vault is locked
.ml-auto(ngbDropdown, placement='bottom-right')
button.btn.btn-link(ngbDropdownToggle, (click)='$event.stopPropagation()')
span(*ngIf='rememberFor') Remember for {{getRememberForDisplay(rememberFor)}}
span(*ngIf='!rememberFor') Do not remember
span(
*ngIf='rememberFor',
translate,
[translateParams]='{time: getRememberForDisplay(rememberFor)}'
) Remember for {time}
span(*ngIf='!rememberFor', translate) Do not remember
div(ngbDropdownMenu)
button.dropdown-item(
(click)='rememberFor = 0',
translate
) Do not remember
button.dropdown-item(
*ngFor='let x of rememberOptions',

View File

@@ -4,21 +4,21 @@
h1.tabby-title Tabby
sup α
.text-center.mb-5 Thank you for downloading Tabby!
.text-center.mb-5(translate) Thank you for downloading Tabby!
.form-line
.header
.title Enable analytics
.description Help track the number of Tabby installs across the world!
.title(translate) Enable analytics
.description(translate) Help track the number of Tabby installs across the world!
toggle([(ngModel)]='config.store.enableAnalytics')
.form-line
.header
.title Enable global hotkey (#[strong Ctrl-Space])
.description Toggles the Tabby window visibility
.title(translate) Enable global hotkey (Ctrl-Space)
.description(translate) Toggles the Tabby window visibility
toggle([(ngModel)]='enableGlobalHotkey')
.text-center.mt-5
button.btn.btn-primary((click)='closeAndDisable()') Close and never show again
button.btn.btn-primary((click)='closeAndDisable()', translate) Close and never show again

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component } from '@angular/core'
import { TranslateService } from '@ngx-translate/core'
import { BaseTabComponent } from './baseTab.component'
import { ConfigService } from '../services/config.service'
import { HostWindowService } from '../api/hostWindow'
@@ -16,9 +17,10 @@ export class WelcomeTabComponent extends BaseTabComponent {
constructor (
private hostWindow: HostWindowService,
public config: ConfigService,
translate: TranslateService,
) {
super()
this.setTitle('Welcome')
this.setTitle(translate.instant('Welcome'))
}
async closeAndDisable () {

View File

@@ -38,3 +38,4 @@ enableExperimentalFeatures: false
pluginBlacklist: []
hacks:
disableGPU: false
language: null

View File

@@ -1,4 +1,5 @@
import { Injectable } from '@angular/core'
import { TranslateService } from '@ngx-translate/core'
import { ProfilesService } from './services/profiles.service'
import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
import { PartialProfile, Profile } from './api'
@@ -9,188 +10,189 @@ export class AppHotkeyProvider extends HotkeyProvider {
hotkeys: HotkeyDescription[] = [
{
id: 'profile-selector',
name: 'Show profile selector',
name: this.translate.instant('Show profile selector'),
},
{
id: 'toggle-fullscreen',
name: 'Toggle fullscreen mode',
name: this.translate.instant('Toggle fullscreen mode'),
},
{
id: 'rename-tab',
name: 'Rename Tab',
name: this.translate.instant('Rename Tab'),
},
{
id: 'close-tab',
name: 'Close tab',
name: this.translate.instant('Close tab'),
},
{
id: 'reopen-tab',
name: 'Reopen last tab',
name: this.translate.instant('Reopen last tab'),
},
{
id: 'toggle-last-tab',
name: 'Toggle last tab',
name: this.translate.instant('Toggle last tab'),
},
{
id: 'next-tab',
name: 'Next tab',
name: this.translate.instant('Next tab'),
},
{
id: 'previous-tab',
name: 'Previous tab',
name: this.translate.instant('Previous tab'),
},
{
id: 'move-tab-left',
name: 'Move tab to the left',
name: this.translate.instant('Move tab to the left'),
},
{
id: 'move-tab-right',
name: 'Move tab to the right',
name: this.translate.instant('Move tab to the right'),
},
{
id: 'rearrange-panes',
name: 'Show pane labels (for rearranging)',
name: this.translate.instant('Show pane labels (for rearranging)'),
},
{
id: 'duplicate-tab',
name: 'Duplicate tab',
name: this.translate.instant('Duplicate tab'),
},
{
id: 'tab-1',
name: 'Tab 1',
name: this.translate.instant('Tab 1'),
},
{
id: 'tab-2',
name: 'Tab 2',
name: this.translate.instant('Tab 2'),
},
{
id: 'tab-3',
name: 'Tab 3',
name: this.translate.instant('Tab 3'),
},
{
id: 'tab-4',
name: 'Tab 4',
name: this.translate.instant('Tab 4'),
},
{
id: 'tab-5',
name: 'Tab 5',
name: this.translate.instant('Tab 5'),
},
{
id: 'tab-6',
name: 'Tab 6',
name: this.translate.instant('Tab 6'),
},
{
id: 'tab-7',
name: 'Tab 7',
name: this.translate.instant('Tab 7'),
},
{
id: 'tab-8',
name: 'Tab 8',
name: this.translate.instant('Tab 8'),
},
{
id: 'tab-9',
name: 'Tab 9',
name: this.translate.instant('Tab 9'),
},
{
id: 'tab-10',
name: 'Tab 10',
name: this.translate.instant('Tab 10'),
},
{
id: 'tab-11',
name: 'Tab 11',
name: this.translate.instant('Tab 11'),
},
{
id: 'tab-12',
name: 'Tab 12',
name: this.translate.instant('Tab 12'),
},
{
id: 'tab-13',
name: 'Tab 13',
name: this.translate.instant('Tab 13'),
},
{
id: 'tab-14',
name: 'Tab 14',
name: this.translate.instant('Tab 14'),
},
{
id: 'tab-15',
name: 'Tab 15',
name: this.translate.instant('Tab 15'),
},
{
id: 'tab-16',
name: 'Tab 16',
name: this.translate.instant('Tab 16'),
},
{
id: 'tab-17',
name: 'Tab 17',
name: this.translate.instant('Tab 17'),
},
{
id: 'tab-18',
name: 'Tab 18',
name: this.translate.instant('Tab 18'),
},
{
id: 'tab-19',
name: 'Tab 19',
name: this.translate.instant('Tab 19'),
},
{
id: 'tab-20',
name: 'Tab 20',
name: this.translate.instant('Tab 20'),
},
{
id: 'split-right',
name: 'Split to the right',
name: this.translate.instant('Split to the right'),
},
{
id: 'split-bottom',
name: 'Split to the bottom',
name: this.translate.instant('Split to the bottom'),
},
{
id: 'split-left',
name: 'Split to the left',
name: this.translate.instant('Split to the left'),
},
{
id: 'split-top',
name: 'Split to the top',
name: this.translate.instant('Split to the top'),
},
{
id: 'pane-maximize',
name: 'Maximize the active pane',
name: this.translate.instant('Maximize the active pane'),
},
{
id: 'pane-nav-up',
name: 'Focus the pane above',
name: this.translate.instant('Focus the pane above'),
},
{
id: 'pane-nav-down',
name: 'Focus the pane below',
name: this.translate.instant('Focus the pane below'),
},
{
id: 'pane-nav-left',
name: 'Focus the pane on the left',
name: this.translate.instant('Focus the pane on the left'),
},
{
id: 'pane-nav-right',
name: 'Focus the pane on the right',
name: this.translate.instant('Focus the pane on the right'),
},
{
id: 'pane-nav-previous',
name: 'Focus previous pane',
name: this.translate.instant('Focus previous pane'),
},
{
id: 'pane-nav-next',
name: 'Focus next pane',
name: this.translate.instant('Focus next pane'),
},
{
id: 'switch-profile',
name: 'Switch profile in the active pane',
name: this.translate.instant('Switch profile in the active pane'),
},
{
id: 'close-pane',
name: 'Close focused pane',
name: this.translate.instant('Close focused pane'),
},
]
constructor (
private profilesService: ProfilesService,
private translate: TranslateService,
) { super() }
async provide (): Promise<HotkeyDescription[]> {

View File

@@ -1,4 +1,4 @@
import { NgModule, ModuleWithProviders } from '@angular/core'
import { NgModule, ModuleWithProviders, LOCALE_ID } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { FormsModule } from '@angular/forms'
@@ -7,6 +7,8 @@ import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-sc
import { NgxFilesizeModule } from 'ngx-filesize'
import { SortablejsModule } from 'ngx-sortablejs'
import { DragDropModule } from '@angular/cdk/drag-drop'
import { TranslateModule, TranslateCompiler, TranslateService } from '@ngx-translate/core'
import { TranslateMessageFormatCompiler, MESSAGE_FORMAT_CONFIG } from 'ngx-translate-messageformat-compiler'
import { AppRootComponent } from './components/appRoot.component'
import { CheckboxComponent } from './components/checkbox.component'
@@ -40,6 +42,7 @@ import { AppService } from './services/app.service'
import { ConfigService } from './services/config.service'
import { VaultFileProvider } from './services/vault.service'
import { HotkeysService } from './services/hotkeys.service'
import { LocaleService, TranslateServiceWrapper } from './services/locale.service'
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
import { CoreConfigProvider } from './config'
@@ -51,6 +54,10 @@ import { SplitLayoutProfilesService } from './profiles'
import 'perfect-scrollbar/css/perfect-scrollbar.css'
export function TranslateMessageFormatCompilerFactory (): TranslateMessageFormatCompiler {
return new TranslateMessageFormatCompiler()
}
const PROVIDERS = [
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
{ provide: Theme, useClass: StandardTheme, multi: true },
@@ -68,6 +75,19 @@ const PROVIDERS = [
{ provide: FileProvider, useClass: VaultFileProvider, multi: true },
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
{ provide: ProfileProvider, useExisting: SplitLayoutProfilesService, multi: true },
{
provide: LOCALE_ID,
deps: [LocaleService],
useFactory: locale => locale.getLocale(),
},
{
provide: MESSAGE_FORMAT_CONFIG,
useValue: LocaleService.allLocales,
},
{
provide: TranslateService,
useClass: TranslateServiceWrapper,
},
]
/** @hidden */
@@ -81,6 +101,7 @@ const PROVIDERS = [
PerfectScrollbarModule,
DragDropModule,
SortablejsModule.forRoot({ animation: 150 }),
TranslateModule,
],
declarations: [
AppRootComponent,
@@ -127,6 +148,7 @@ const PROVIDERS = [
AlwaysVisibleTypeaheadDirective,
SortablejsModule,
DragDropModule,
TranslateModule,
],
})
export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
@@ -135,6 +157,8 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
config: ConfigService,
platform: PlatformService,
hotkeys: HotkeysService,
public locale: LocaleService,
private translate: TranslateService,
private profilesService: ProfilesService,
private selector: SelectorService,
) {
@@ -182,8 +206,8 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
if (provider.supportsQuickConnect) {
options.push({
name: 'Quick connect',
freeInputPattern: 'Connect to "%s"...',
name: this.translate.instant('Quick connect'),
freeInputPattern: this.translate.instant('Connect to "%s"...'),
icon: 'fas fa-arrow-right',
callback: query => {
const p = provider.quickConnect(query)
@@ -194,13 +218,23 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
})
}
await this.selector.show('Select profile', options)
await this.selector.show(this.translate.instant('Select profile'), options)
}
static forRoot (): ModuleWithProviders<AppModule> {
const translateModule = TranslateModule.forRoot({
defaultLanguage: 'en',
compiler: {
provide: TranslateCompiler,
useFactory: TranslateMessageFormatCompilerFactory,
},
})
return {
ngModule: AppModule,
providers: PROVIDERS,
providers: [
...PROVIDERS,
...translateModule.providers!.filter(x => x !== TranslateService),
],
}
}
}

View File

@@ -1,6 +1,7 @@
import slugify from 'slugify'
import { v4 as uuidv4 } from 'uuid'
import { Injectable } from '@angular/core'
import { TranslateService } from '@ngx-translate/core'
import { ConfigService, NewTabParameters, PartialProfile, Profile, ProfileProvider } from './api'
import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component'
@@ -15,7 +16,7 @@ export interface SplitLayoutProfile extends Profile {
@Injectable({ providedIn: 'root' })
export class SplitLayoutProfilesService extends ProfileProvider<SplitLayoutProfile> {
id = 'split-layout'
name = 'Saved layout'
name = this.translate.instant('Saved layout')
configDefaults = {
options: {
recoveryToken: null,
@@ -25,6 +26,7 @@ export class SplitLayoutProfilesService extends ProfileProvider<SplitLayoutProfi
constructor (
private splitTabRecoveryProvider: SplitTabRecoveryProvider,
private config: ConfigService,
private translate: TranslateService,
) {
super()
}

View File

@@ -4,6 +4,7 @@ import { v4 as uuidv4 } from 'uuid'
import * as yaml from 'js-yaml'
import { Observable, Subject, AsyncSubject } from 'rxjs'
import { Injectable, Inject } from '@angular/core'
import { TranslateService } from '@ngx-translate/core'
import { ConfigProvider } from '../api/configProvider'
import { PlatformService } from '../api/platform'
import { HostAppService } from '../api/hostApp'
@@ -136,6 +137,7 @@ export class ConfigService {
private hostApp: HostAppService,
private platform: PlatformService,
private vault: VaultService,
private translate: TranslateService,
@Inject(ConfigProvider) private configProviders: ConfigProvider[],
) {
this.defaults = this.mergeDefaults()
@@ -360,9 +362,13 @@ export class ConfigService {
} catch (e) {
let result = await this.platform.showMessageBox({
type: 'error',
message: 'Could not decrypt config',
message: this.translate.instant('Could not decrypt config'),
detail: e.toString(),
buttons: ['Try again', 'Erase config', 'Quit'],
buttons: [
this.translate.instant('Try again'),
this.translate.instant('Erase config'),
this.translate.instant('Quit'),
],
defaultId: 0,
})
if (result.response === 2) {
@@ -371,9 +377,12 @@ export class ConfigService {
if (result.response === 1) {
result = await this.platform.showMessageBox({
type: 'warning',
message: 'Are you sure?',
message: this.translate.instant('Are you sure?'),
detail: e.toString(),
buttons: ['Erase config', 'Quit'],
buttons: [
this.translate.instant('Erase config'),
this.translate.instant('Quit'),
],
defaultId: 1,
cancelId: 1,
})

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@angular/core'
import { TranslateService } from '@ngx-translate/core'
import { FileProvider, NotificationsService, SelectorService } from '../api'
@Injectable({ providedIn: 'root' })
@@ -7,6 +8,7 @@ export class FileProvidersService {
private constructor (
private selector: SelectorService,
private notifications: NotificationsService,
private translate: TranslateService,
@Inject(FileProvider) private fileProviders: FileProvider[],
) { }
@@ -34,15 +36,18 @@ export class FileProvidersService {
}
}))
if (!providers.length) {
this.notifications.error('Vault master passphrase needs to be set to allow storing secrets')
this.notifications.error(this.translate.instant('Vault master passphrase needs to be set to allow storing secrets'))
throw new Error('No available file providers')
}
if (providers.length === 1) {
return providers[0]
}
return this.selector.show('Select file storage', providers.map(p => ({
name: p.name,
result: p,
})))
return this.selector.show(
this.translate.instant('Select file storage'),
providers.map(p => ({
name: p.name,
result: p,
}))
)
}
}

View File

@@ -0,0 +1,134 @@
import { Injectable } from '@angular/core'
import { registerLocaleData } from '@angular/common'
import { TranslateService } from '@ngx-translate/core'
import localeEN from '@angular/common/locales/en-GB'
import localeRU from '@angular/common/locales/ru'
import { Observable, Subject } from 'rxjs'
import { distinctUntilChanged } from 'rxjs/operators'
import { ConfigService } from './config.service'
import { LogService, Logger } from './log.service'
registerLocaleData(localeEN)
registerLocaleData(localeRU)
@Injectable({ providedIn: 'root' })
export class TranslateServiceWrapper extends TranslateService {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
getParsedResult (translations: any, key: any, interpolateParams?: any): any {
this.translations[this.defaultLang][key] ??= this.compiler.compile(key, this.defaultLang)
return super.getParsedResult(translations, key, interpolateParams)
}
}
@Injectable({ providedIn: 'root' })
export class LocaleService {
private logger: Logger
static readonly allLocales = ['en', 'de', 'fr', 'ru']
get localeChanged$ (): Observable<string> {
return this.localeChanged.pipe(distinctUntilChanged())
}
get catalogChanged$ (): Observable<Record<string, string | undefined>> {
return this.catalogChanged.pipe(distinctUntilChanged())
}
readonly allLanguages: { code: string, name: string }[]
private translations = {
en: {
Close: 'Close',
},
ru: {
Close: 'Закрыть',
},
}
private locale = 'en'
private localeChanged = new Subject<string>()
private catalogChanged = new Subject<Record<string, string | undefined>>()
constructor (
private config: ConfigService,
private translate: TranslateService,
log: LogService,
) {
this.logger = log.create('translate')
config.changed$.subscribe(() => {
this.refresh()
})
config.ready$.subscribe(() => {
this.refresh()
})
this.allLanguages = [
{
code: 'en',
name: translate.instant('English'),
},
{
code: 'de',
name: translate.instant('German'),
},
{
code: 'fr',
name: translate.instant('French'),
},
/* {
code: 'it',
name: translate.instant('Italian'),
},
{
code: 'es',
name: translate.instant('Spanish'),
}, */
{
code: 'ru',
name: translate.instant('Russian'),
},
/* {
code: 'ar',
name: translate.instant('Arabic'),
}, */
]
}
refresh (): void {
let lang = this.config.store.language
if (!lang) {
const systemLanguage = navigator.language.toLowerCase().split('-')[0]
if (this.allLanguages.some(x => x.code === systemLanguage)) {
lang = systemLanguage
}
}
lang ??= 'en'
this.setLocale(lang)
}
async setLocale (lang: string): Promise<void> {
const strings = this.translations[lang]
if (!this.translate.langs.includes(lang)) {
this.translate.addLangs([lang])
const po = require(`../../../locale/${lang}.po`).translations['']
const translation = {}
for (const k of Object.keys(po)) {
translation[k] = po[k].msgstr[0] || k
}
this.translate.setTranslation(lang, translation)
}
this.translate.setDefaultLang(lang)
this.locale = lang
this.localeChanged.next(lang)
this.logger.debug('Setting language to', lang)
this.catalogChanged.next(strings)
}
getLocale (): string {
return this.locale
}
}

View File

@@ -1,4 +1,5 @@
import { Injectable, Inject } from '@angular/core'
import { TranslateService } from '@ngx-translate/core'
import { NewTabParameters } from './tabs.service'
import { BaseTabComponent } from '../components/baseTab.component'
import { PartialProfile, Profile, ProfileProvider } from '../api/profileProvider'
@@ -29,6 +30,7 @@ export class ProfilesService {
private config: ConfigService,
private notifications: NotificationsService,
private selector: SelectorService,
private translate: TranslateService,
@Inject(ProfileProvider) private profileProviders: ProfileProvider<Profile>[],
) { }
@@ -103,7 +105,7 @@ export class ProfilesService {
let options: SelectorOption<void>[] = recentProfiles.map(p => ({
...this.selectorOptionForProfile(p),
group: 'Recent',
group: this.translate.instant('Recent'),
icon: 'fas fa-history',
color: p.color,
callback: async () => {
@@ -115,8 +117,8 @@ export class ProfilesService {
}))
if (recentProfiles.length) {
options.push({
name: 'Clear recent profiles',
group: 'Recent',
name: this.translate.instant('Clear recent profiles'),
group: this.translate.instant('Recent'),
icon: 'fas fa-eraser',
callback: async () => {
window.localStorage.removeItem('recentProfiles')
@@ -142,7 +144,7 @@ export class ProfilesService {
try {
const { SettingsTabComponent } = window['nodeRequire']('tabby-settings')
options.push({
name: 'Manage profiles',
name: this.translate.instant('Manage profiles'),
icon: 'fas fa-window-restore',
callback: () => {
this.app.openNewTabRaw({
@@ -156,8 +158,8 @@ export class ProfilesService {
if (this.getProviders().some(x => x.supportsQuickConnect)) {
options.push({
name: 'Quick connect',
freeInputPattern: 'Connect to "%s"...',
name: this.translate.instant('Quick connect'),
freeInputPattern: this.translate.instant('Connect to "%s"...'),
icon: 'fas fa-arrow-right',
callback: query => {
const profile = this.quickConnect(query)
@@ -165,7 +167,7 @@ export class ProfilesService {
},
})
}
await this.selector.show('Select profile or enter an address', options)
await this.selector.show(this.translate.instant('Select profile or enter an address'), options)
} catch (err) {
reject(err)
}

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Injectable } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { TranslateService } from '@ngx-translate/core'
import { Subscription } from 'rxjs'
import { AppService } from './services/app.service'
import { BaseTabComponent } from './components/baseTab.component'
@@ -22,6 +23,7 @@ export class TabManagementContextMenu extends TabContextMenuItemProvider {
constructor (
private app: AppService,
private translate: TranslateService,
) {
super()
}
@@ -29,7 +31,7 @@ export class TabManagementContextMenu extends TabContextMenuItemProvider {
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
let items: MenuItemOptions[] = [
{
label: 'Close',
label: this.translate.instant('Close'),
click: () => {
if (this.app.tabs.includes(tab)) {
this.app.closeTab(tab, true)
@@ -43,7 +45,7 @@ export class TabManagementContextMenu extends TabContextMenuItemProvider {
items = [
...items,
{
label: 'Close other tabs',
label: this.translate.instant('Close other tabs'),
click: () => {
for (const t of this.app.tabs.filter(x => x !== tab)) {
this.app.closeTab(t, true)
@@ -51,7 +53,7 @@ export class TabManagementContextMenu extends TabContextMenuItemProvider {
},
},
{
label: 'Close tabs to the right',
label: this.translate.instant('Close tabs to the right'),
click: () => {
for (const t of this.app.tabs.slice(this.app.tabs.indexOf(tab) + 1)) {
this.app.closeTab(t, true)
@@ -59,7 +61,7 @@ export class TabManagementContextMenu extends TabContextMenuItemProvider {
},
},
{
label: 'Close tabs to the left',
label: this.translate.instant('Close tabs to the left'),
click: () => {
for (const t of this.app.tabs.slice(0, this.app.tabs.indexOf(tab))) {
this.app.closeTab(t, true)
@@ -71,13 +73,13 @@ export class TabManagementContextMenu extends TabContextMenuItemProvider {
if (tab.parent instanceof SplitTabComponent) {
const directions: SplitDirection[] = ['r', 'b', 'l', 't']
items.push({
label: 'Split',
label: this.translate.instant('Split'),
submenu: directions.map(dir => ({
label: {
r: 'Right',
b: 'Down',
l: 'Left',
t: 'Up',
r: this.translate.instant('Right'),
b: this.translate.instant('Down'),
l: this.translate.instant('Left'),
t: this.translate.instant('Up'),
}[dir],
click: () => {
(tab.parent as SplitTabComponent).splitTab(tab, dir)
@@ -99,6 +101,7 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
private app: AppService,
private ngbModal: NgbModal,
private splitLayoutProfilesService: SplitLayoutProfilesService,
private translate: TranslateService,
) {
super()
}
@@ -109,18 +112,18 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
items = [
...items,
{
label: 'Rename',
label: this.translate.instant('Rename'),
click: () => tabHeader.showRenameTabModal(),
},
{
label: 'Duplicate',
label: this.translate.instant('Duplicate'),
click: () => this.app.duplicateTab(tab),
},
{
label: 'Color',
label: this.translate.instant('Color'),
sublabel: TAB_COLORS.find(x => x.value === tab.color)?.name,
submenu: TAB_COLORS.map(color => ({
label: color.name,
label: this.translate.instant(color.name),
type: 'radio',
checked: tab.color === color.value,
click: () => {
@@ -132,10 +135,10 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
if (tab instanceof SplitTabComponent && tab.getAllTabs().length > 1) {
items.push({
label: 'Save layout as profile',
label: this.translate.instant('Save layout as profile'),
click: async () => {
const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = 'Profile name'
modal.componentInstance.prompt = this.translate.instant('Profile name')
const name = (await modal.result)?.value
if (!name) {
return
@@ -154,6 +157,7 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
constructor (
private app: AppService,
private translate: TranslateService,
) {
super()
}
@@ -167,10 +171,10 @@ export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
if (process) {
items.push({
enabled: false,
label: 'Current process: ' + process.name,
label: this.translate.instant('Current process: {name}', process),
})
items.push({
label: 'Notify when done',
label: this.translate.instant('Notify when done'),
type: 'checkbox',
checked: extTab.__completionNotificationEnabled,
click: () => {
@@ -178,7 +182,7 @@ export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
if (extTab.__completionNotificationEnabled) {
this.app.observeTabCompletion(tab).subscribe(() => {
new Notification('Process completed', {
new Notification(this.translate.instant('Process completed'), {
body: process.name,
}).addEventListener('click', () => {
this.app.selectTab(tab)
@@ -192,7 +196,7 @@ export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
})
}
items.push({
label: 'Notify on activity',
label: this.translate.instant('Notify on activity'),
type: 'checkbox',
checked: !!extTab.__outputNotificationSubscription,
click: () => {
@@ -204,7 +208,7 @@ export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
if (extTab.__outputNotificationSubscription && active) {
extTab.__outputNotificationSubscription.unsubscribe()
extTab.__outputNotificationSubscription = null
new Notification('Tab activity', {
new Notification(this.translate.instant('Tab activity'), {
body: tab.title,
}).addEventListener('click', () => {
this.app.selectTab(tab)
@@ -228,6 +232,7 @@ export class ProfilesContextMenu extends TabContextMenuItemProvider {
private profilesService: ProfilesService,
private tabsService: TabsService,
private app: AppService,
private translate: TranslateService,
hotkeys: HotkeysService,
) {
super()
@@ -270,7 +275,7 @@ export class ProfilesContextMenu extends TabContextMenuItemProvider {
if (!tabHeader && tab.parent instanceof SplitTabComponent && tab.parent.getAllTabs().length > 1) {
return [
{
label: 'Switch profile',
label: this.translate.instant('Switch profile'),
click: () => this.switchTabProfile(tab),
},
]

View File

@@ -1,28 +1,41 @@
import { Injectable } from '@angular/core'
import { TranslateService } from '@ngx-translate/core'
import { Theme } from './api'
/** @hidden */
@Injectable()
export class StandardTheme extends Theme {
name = 'Standard'
name = this.translate.instant('Standard')
css = require('./theme.scss')
terminalBackground = '#222a33'
constructor (private translate: TranslateService) {
super()
}
}
/** @hidden */
@Injectable()
export class StandardCompactTheme extends Theme {
name = 'Compact'
name = this.translate.instant('Compact')
css = require('./theme.compact.scss')
terminalBackground = '#222a33'
macOSWindowButtonsInsetX = 8
macOSWindowButtonsInsetY = 6
constructor (private translate: TranslateService) {
super()
}
}
/** @hidden */
@Injectable()
export class PaperTheme extends Theme {
name = 'Paper'
name = this.translate.instant('Paper')
css = require('./theme.paper.scss')
terminalBackground = '#f7f1e0'
constructor (private translate: TranslateService) {
super()
}
}

View File

@@ -1,5 +1,6 @@
import * as os from 'os'
import { NgZone } from '@angular/core'
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
export const WIN_BUILD_CONPTY_SUPPORTED = 17692
export const WIN_BUILD_CONPTY_STABLE = 18309
@@ -56,13 +57,13 @@ export class ResettableTimeout {
}
export const TAB_COLORS = [
{ name: 'No color', value: null },
{ name: 'Blue', value: '#0275d8' },
{ name: 'Green', value: '#5cb85c' },
{ name: 'Orange', value: '#f0ad4e' },
{ name: 'Purple', value: '#613d7c' },
{ name: 'Red', value: '#d9534f' },
{ name: 'Yellow', value: '#ffd500' },
{ name: _('No color'), value: null },
{ name: _('Blue'), value: '#0275d8' },
{ name: _('Green'), value: '#5cb85c' },
{ name: _('Orange'), value: '#f0ad4e' },
{ name: _('Purple'), value: '#613d7c' },
{ name: _('Red'), value: '#d9534f' },
{ name: _('Yellow'), value: '#ffd500' },
]
export function serializeFunction <T extends () => Promise<any>> (fn: T): T {

View File

@@ -2,6 +2,13 @@
# yarn lockfile v1
"@ngx-translate/core@^14.0.0":
version "14.0.0"
resolved "https://registry.yarnpkg.com/@ngx-translate/core/-/core-14.0.0.tgz#af421d0e1a28376843f0fed375cd2fae7630a5ff"
integrity sha512-UevdwNCXMRCdJv//0kC8h2eSfmi02r29xeE8E9gJ1Al4D4jEJ7eiLPdjslTMc21oJNGguqqWeEVjf64SFtvw2w==
dependencies:
tslib "^2.3.0"
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -56,6 +63,37 @@ js-yaml@^4.0.0:
dependencies:
argparse "^2.0.1"
make-plural@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/make-plural/-/make-plural-4.3.0.tgz#f23de08efdb0cac2e0c9ba9f315b0dff6b4c2735"
integrity sha512-xTYd4JVHpSCW+aqDof6w/MebaMVNTVYBZhbB/vi513xXdiPT92JMVCo0Jq8W2UZnzYRFeVbQiQ+I25l13JuKvA==
optionalDependencies:
minimist "^1.2.0"
messageformat-formatters@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/messageformat-formatters/-/messageformat-formatters-2.0.1.tgz#0492c1402a48775f751c9b17c0354e92be012b08"
integrity sha512-E/lQRXhtHwGuiQjI7qxkLp8AHbMD5r2217XNe/SREbBlSawe0lOqsFb7rflZJmlQFSULNLIqlcjjsCPlB3m3Mg==
messageformat-parser@^4.1.2:
version "4.1.3"
resolved "https://registry.yarnpkg.com/messageformat-parser/-/messageformat-parser-4.1.3.tgz#b824787f57fcda7d50769f5b63e8d4fda68f5b9e"
integrity sha512-2fU3XDCanRqeOCkn7R5zW5VQHWf+T3hH65SzuqRvjatBK7r4uyFa5mEX+k6F9Bd04LVM5G4/BHBTUJsOdW7uyg==
messageformat@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/messageformat/-/messageformat-2.3.0.tgz#de263c49029d5eae65d7ee25e0754f57f425ad91"
integrity sha512-uTzvsv0lTeQxYI2y1NPa1lItL5VRI8Gb93Y2K2ue5gBPyrbJxfDi/EYWxh2PKv5yO42AJeeqblS9MJSh/IEk4w==
dependencies:
make-plural "^4.3.0"
messageformat-formatters "^2.0.1"
messageformat-parser "^4.1.2"
minimist@^1.2.0:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
mixpanel@^0.13.0:
version "0.13.0"
resolved "https://registry.yarnpkg.com/mixpanel/-/mixpanel-0.13.0.tgz#699bf510d9ba013c75edcf979ff1e24085fde9d2"
@@ -85,6 +123,13 @@ ngx-perfect-scrollbar@^10.1.0:
resize-observer-polyfill "^1.5.0"
tslib "^2.0.0"
ngx-translate-messageformat-compiler@^4.11.0:
version "4.11.0"
resolved "https://registry.yarnpkg.com/ngx-translate-messageformat-compiler/-/ngx-translate-messageformat-compiler-4.11.0.tgz#c9b71dd139ba5fcdcd809001e22622de589fd707"
integrity sha512-OdGfWV4fF3DhZqGIHcLmOnQDufugmZ+E90NYr1UPGRZgT10lilr9oLmIrisy3lW4THnZFNo9JXsX7+fX84LbDw==
dependencies:
tslib "^1.10.0"
perfect-scrollbar@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/perfect-scrollbar/-/perfect-scrollbar-1.5.0.tgz#821d224ed8ff61990c23f26db63048cdc75b6b83"
@@ -116,11 +161,21 @@ string_decoder@^1.1.1:
dependencies:
safe-buffer "~5.2.0"
tslib@^1.10.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
tslib@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"