Skip to content

Commit

Permalink
feat: translation server provides project name and source file path
Browse files Browse the repository at this point in the history
  • Loading branch information
kyubisation committed Jan 2, 2020
1 parent 6b89208 commit c5823e5
Show file tree
Hide file tree
Showing 17 changed files with 116 additions and 148 deletions.
11 changes: 7 additions & 4 deletions builders/t9n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,19 @@ export async function t9n(options: Options, context: BuilderContext): Promise<Bu

const translationContext = await TranslationFactory.createTranslationContext({
logger: context.logger,
source: translationFile,
project: context.target ? context.target.project : 'unknown',
sourceFile: translationFile,
targetPath: targetTranslationPath,
targets: targetFiles.map(f => join(targetTranslationPath, f)),
includeContextInTarget: options.includeContextInTarget,
encoding: options.encoding || 'UTF-8'
});
const server = new TranslationServer(context.logger, translationContext, options.port);
await server.shutdown.toPromise();
const server = new TranslationServer(context.logger, translationContext);
server.listen(options.port, () =>
context.logger.info(`Translation server started: http://localhost:${options.port}\n`)
);

return { success: true };
return new Promise(() => {});
}

function isFile(path: string) {
Expand Down
4 changes: 4 additions & 0 deletions builders/t9n/server/responses/root-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import { Hal, HalLink, Links } from '../hal';
import { UrlFactory } from '../url-factory';

export class RootResponse implements Hal {
project: string;
sourceFile: string;
sourceLanguage: string;
languages: string[];
unitCount: number;
_links?: { [key: string]: HalLink };
_embedded?: { [key: string]: unknown };

constructor(context: TranslationContext, urlFactory: UrlFactory) {
this.project = context.project;
this.sourceFile = context.source.file;
this.sourceLanguage = context.source.language;
this.languages = context.languages;
this.unitCount = context.source.units.length;
Expand Down
37 changes: 6 additions & 31 deletions builders/t9n/server/translation-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import { readFile } from 'fs';
import Koa from 'koa';
import koaBody from 'koa-body';
import koaStatic from 'koa-static';
import { Server, Socket } from 'net';
import { Server } from 'net';
import { join } from 'path';
import { Observable, Subject } from 'rxjs';
import { promisify } from 'util';

import { TranslationContext, TranslationTargetUnit } from '../translation';
Expand All @@ -34,32 +33,18 @@ import {
const readFileAsync = promisify(readFile);

export class TranslationServer {
readonly shutdown: Observable<void>;

private _connections: { [key: string]: Socket } = {};
private _shutdown = new Subject<void>();
private _server: Server;
private readonly _server: Koa<any, any>;

constructor(
private readonly _logger: logging.LoggerApi,
private readonly _context: TranslationContext,
private readonly _port: number
private readonly _context: TranslationContext
) {
this._logger.info(`Current languages: ${this._context.languages.join(', ')}\n`);
this._server = this._createServer();
this.shutdown = this._shutdown.asObservable();
this._server = this._createApp();
}

private _createServer() {
const server = this._createApp().listen(this._port, () =>
this._logger.info(`Translation server started: http://localhost:${this._port}\n`)
);
server.on('connection', connection => {
const key = `${connection.remoteAddress}:${connection.remotePort}`;
this._connections[key] = connection;
connection.on('close', () => delete this._connections[key]);
});
return server;
listen(port: number, listeningListener?: () => void): Server {
return this._server.listen(port, listeningListener);
}

private _createApp() {
Expand All @@ -84,16 +69,6 @@ export class TranslationServer {
options?: Router.UrlOptionsQuery
) => `${ctx.protocol}://${ctx.host}${ctx.router.url(name, params, options)}`;
return new Router({ prefix: '/api' })
.delete('/', async () => {
this._logger.info('\nClosing connections');
await new Promise((resolve, reject) => {
this._server.close(err => (err ? reject(err) : resolve()));
Object.values(this._connections).forEach(c => c.destroy());
});
this._logger.info('Shutting down translation server');
this._shutdown.next();
this._shutdown.complete();
})
.get(ROOT_ROUTE, '/', ctx => {
ctx.body = new RootResponse(this._context, toUrlFactory(ctx));
})
Expand Down
20 changes: 20 additions & 0 deletions builders/t9n/translation/debounce-scheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export class DebounceScheduler<T> {
private readonly _scheduleMap = new Map<T, NodeJS.Timeout>();

constructor(
private readonly _action: (trigger: T) => Promise<void>,
private readonly _debounceTime = 500
) {}

schedule(trigger: T) {
const entry = this._scheduleMap.get(trigger);
if (entry !== undefined) {
clearTimeout(entry);
}

this._scheduleMap.set(
trigger,
setTimeout(() => this._action(trigger), this._debounceTime)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ export interface TranslationDeserializer {
file: string,
encoding: string
): Promise<{
sourceLanguage: string;
language: string;
original: string;
unitMap: Map<string, TranslationSourceUnit>;
}>;
deserializeTarget(
file: string,
encoding: string
): Promise<{ targetLanguage: string; unitMap: Map<string, TranslationTargetUnit> }>;
): Promise<{ language: string; unitMap: Map<string, TranslationTargetUnit> }>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ export abstract class XlfDeserializerBase implements TranslationDeserializer {
file: string,
encoding: string
): Promise<{
sourceLanguage: string;
language: string;
original: string;
unitMap: Map<string, TranslationSourceUnit>;
}>;

abstract deserializeTarget(
file: string,
encoding: string
): Promise<{ targetLanguage: string; unitMap: Map<string, TranslationTargetUnit> }>;
): Promise<{ language: string; unitMap: Map<string, TranslationTargetUnit> }>;

protected async _createDocument(file: string, encoding: string) {
const content = await readFileAsync(file, encoding || 'utf8');
Expand Down
10 changes: 5 additions & 5 deletions builders/t9n/translation/deserialization/xlf-deserializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,29 @@ export class XlfDeserializer extends XlfDeserializerBase {
async deserializeSource(file: string, encoding: string) {
const doc = await this._createDocument(file, encoding);
const fileNode = this._getFileNode(doc);
const sourceLanguage = fileNode.getAttribute('source-language')!;
const language = fileNode.getAttribute('source-language')!;
const original = fileNode.getAttribute('original') || '';
const unitMap = Array.from(fileNode.getElementsByTagName('trans-unit'))
.map(u => this._deserializeSourceUnit(u))
.reduce(
(current, next) => current.set(next.id, next),
new Map<string, TranslationSourceUnit>()
);
return { sourceLanguage, original, unitMap };
return { language, original, unitMap };
}

async deserializeTarget(file: string, encoding: string) {
const doc = await this._createDocument(file, encoding);
const fileNode = this._getFileNode(doc);
const targetLanguage = fileNode.getAttribute('target-language')!;
this._assertTargetLanguage(targetLanguage, file);
const language = fileNode.getAttribute('target-language')!;
this._assertTargetLanguage(language, file);
const unitMap = Array.from(fileNode.getElementsByTagName('trans-unit'))
.map(u => this._deserializeTargetUnit(u))
.reduce(
(current, next) => current.set(next.id, next),
new Map<string, TranslationTargetUnit>()
);
return { targetLanguage, unitMap };
return { language, unitMap };
}

protected _assertXliff(doc: Document) {
Expand Down
10 changes: 5 additions & 5 deletions builders/t9n/translation/deserialization/xlf2-deserializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { XlfDeserializerBase } from './xlf-deserializer-base';
export class Xlf2Deserializer extends XlfDeserializerBase {
async deserializeSource(file: string, encoding: string) {
const doc = await this._createDocument(file, encoding);
const sourceLanguage = doc.documentElement.getAttribute('srcLang')!;
const language = doc.documentElement.getAttribute('srcLang')!;
const fileNode = this._getFileNode(doc);
const original = fileNode.getAttribute('original') || '';
const unitMap = Array.from(fileNode.childNodes)
Expand All @@ -16,13 +16,13 @@ export class Xlf2Deserializer extends XlfDeserializerBase {
(current, next) => current.set(next.id, next),
new Map<string, TranslationSourceUnit>()
);
return { sourceLanguage, original, unitMap };
return { language, original, unitMap };
}

async deserializeTarget(file: string, encoding: string) {
const doc = await this._createDocument(file, encoding);
const targetLanguage = doc.documentElement.getAttribute('trgLang')!;
this._assertTargetLanguage(targetLanguage, file);
const language = doc.documentElement.getAttribute('trgLang')!;
this._assertTargetLanguage(language, file);
const fileNode = this._getFileNode(doc);
const unitMap = Array.from(fileNode.childNodes)
.filter(c => c.nodeName === 'unit')
Expand All @@ -31,7 +31,7 @@ export class Xlf2Deserializer extends XlfDeserializerBase {
(current, next) => current.set(next.id, next),
new Map<string, TranslationTargetUnit>()
);
return { targetLanguage, unitMap };
return { language, unitMap };
}

protected _assertXliff(doc: Document) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { TranslationTarget } from '../translation-target';

export interface TranslationSerializer {
serializeTarget(
file: string,
target: TranslationTarget,
options: {
encoding: string;
Expand Down
3 changes: 1 addition & 2 deletions builders/t9n/translation/serialization/xlf-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const writeFileAsync = promisify(writeFile);

export class XlfSerializer implements TranslationSerializer {
async serializeTarget(
file: string,
target: TranslationTarget,
options: {
encoding: string;
Expand Down Expand Up @@ -40,7 +39,7 @@ ${units
</file>
</xliff>
`;
await writeFileAsync(file, content + EOL, options.encoding);
await writeFileAsync(target.file, content + EOL, options.encoding);
}

private _transformState(state: string) {
Expand Down
3 changes: 1 addition & 2 deletions builders/t9n/translation/serialization/xlf2-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const writeFileAsync = promisify(writeFile);

export class Xlf2Serializer implements TranslationSerializer {
async serializeTarget(
file: string,
target: TranslationTarget,
options: {
encoding: string;
Expand Down Expand Up @@ -40,7 +39,7 @@ ${units
</file>
</xliff>
`;
await writeFileAsync(file, content + EOL, options.encoding);
await writeFileAsync(target.file, content + EOL, options.encoding);
}

private _serializeNotes(unit: TranslationTargetUnit, target: TranslationTarget) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { logging } from '@angular-devkit/core';

import { TranslationSerializer } from './serialization/translation-serializer';
import { TranslationSource } from './translation-source';
import { TranslationTarget } from './translation-target';

export interface TranslationContextConfiguration {
logger: logging.LoggerApi;
project: string;
encoding: string;
includeContextInTarget: boolean;
source: TranslationSource;
sourceFile: string;
targets: Map<string, TranslationTarget>;
serializer: TranslationSerializer;
original: string;
filenameFactory: (target: TranslationTarget) => string;
filenameFactory: (language: string) => string;
}
57 changes: 20 additions & 37 deletions builders/t9n/translation/translation-context.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,46 @@
import { logging } from '@angular-devkit/core';
import { Subject } from 'rxjs';
import { debounceTime, map, switchMap } from 'rxjs/operators';

import { DebounceScheduler } from './debounce-scheduler';
import { TranslationSerializer } from './serialization/translation-serializer';
import { TranslationContextConfiguration } from './translation-context-configuration';
import { TranslationSource } from './translation-source';
import { TranslationTarget } from './translation-target';
import { TranslationTargetUnit } from './translation-target-unit';

export class TranslationContext {
readonly project: string;
readonly source: TranslationSource;
readonly sourceFile: string;

private readonly _logger: logging.LoggerApi;
private readonly _serializer: TranslationSerializer;
private readonly _targets: Map<string, TranslationTarget>;
private readonly _options: {
encoding: string;
original: string;
includeContextInTarget: boolean;
};
private readonly _filenameFactory: (target: TranslationTarget) => string;
private readonly _serializeScheduler = new Map<string, Subject<void>>();
private readonly _filenameFactory: (language: string) => string;
private readonly _serializeScheduler: DebounceScheduler<string>;

get languages() {
return Array.from(this._targets.keys()).sort();
}

constructor(
private readonly _logger: logging.LoggerApi,
configuration: TranslationContextConfiguration
) {
this._serializer = configuration.serializer;
constructor(configuration: TranslationContextConfiguration) {
this.project = configuration.project;
this.source = configuration.source;
this.sourceFile = configuration.sourceFile;
this._logger = configuration.logger;
this._serializer = configuration.serializer;
this._targets = configuration.targets;
this._filenameFactory = configuration.filenameFactory;
this._options = { ...configuration };
this._serializeScheduler = new DebounceScheduler<string>(async language => {
const target = this._targets.get(language)!;
await this._serializer.serializeTarget(target, this._options);
this._logger.info(`${this._timestamp()}: Updated ${target.file}`);
});
}

target(language: string) {
Expand All @@ -45,9 +52,9 @@ export class TranslationContext {
throw new Error(`${language} already exists as target!`);
}

const target = new TranslationTarget(this.source, language);
const file = await this._serialize(target);
this._logger.info(`${this._timestamp()}: Created ${file}`);
const target = new TranslationTarget(this.source, this._filenameFactory(language), language);
await this._serializer.serializeTarget(target, this._options);
this._logger.info(`${this._timestamp()}: Created ${target.file}`);
this._targets.set(language, target);
return target;
}
Expand All @@ -61,34 +68,10 @@ export class TranslationContext {

existingUnit.target = unit.target;
existingUnit.state = unit.state;
this._scheduleSerialization(language);
this._serializeScheduler.schedule(language);
return existingUnit;
}

private _scheduleSerialization(language: string) {
const subject = this._serializeScheduler.get(language) || this._createScheduler(language);
subject.next();
}

private _createScheduler(language: string) {
const subject = new Subject<void>();
subject
.pipe(
debounceTime(500),
map(() => this._targets.get(language)!),
switchMap(t => this._serialize(t))
)
.subscribe(f => this._logger.info(`${this._timestamp()}: Updated ${f}`));
this._serializeScheduler.set(language, subject);
return subject;
}

private async _serialize(target: TranslationTarget) {
const file = this._filenameFactory(target);
await this._serializer.serializeTarget(file, target, this._options);
return file;
}

private _timestamp() {
const now = new Date();
const pad = (value: number) => value.toString().padStart(2, '0');
Expand Down
Loading

0 comments on commit c5823e5

Please sign in to comment.