otus-localization
Version:
A translation tool for Angular i18n(angular-t9n)
352 lines (338 loc) • 16.1 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var child_process = require('child_process');
var path = require('path');
var architect = require('@angular-devkit/architect');
var core = require('@angular-devkit/core');
var node = require('@angular-devkit/core/node');
var common = require('@nestjs/common');
var core$1 = require('@nestjs/core');
var platformWs = require('@nestjs/platform-ws');
var server = require('../../server');
class AngularI18n {
constructor(_host, _workspaceRoot, _projectName, _targetPathBuilder, _translationContextFactory) {
this._host = _host;
this._workspaceRoot = _workspaceRoot;
this._projectName = _projectName;
this._targetPathBuilder = _targetPathBuilder;
this._translationContextFactory = _translationContextFactory;
}
async sourceLocale() {
const i18n = await this._readProjectI18n();
return typeof i18n.sourceLocale === 'object'
? i18n.sourceLocale
: { code: i18n.sourceLocale || '' };
}
async locales() {
const i18n = await this._readProjectI18n();
const locales = i18n.locales || {};
return Object.keys(locales)
.sort()
.reduce((current, next) => Object.assign(current, {
[next]: this._normalizeI18nLocale(locales[next]),
}), {});
}
async update() {
var _a;
const { source, targetRegistry } = this._translationContextFactory();
const i18n = await this._readProjectI18n();
if (typeof i18n.sourceLocale === 'object' || source.baseHref) {
i18n.sourceLocale = {
code: source.language,
baseHref: source.baseHref ||
(typeof i18n.sourceLocale === 'object'
? i18n.sourceLocale.baseHref
: `/${source.language}/`),
};
}
else {
i18n.sourceLocale = source.language;
}
const locales = (_a = i18n.locales) !== null && _a !== void 0 ? _a : {};
i18n.locales = targetRegistry
.values()
.sort((a, b) => a.language.localeCompare(b.language))
.reduce((current, next) => Object.assign(current, {
[next.language]: this._i18nLocale(next, locales[next.language]),
}), {});
const { project, workspace } = await this._readProject();
// Cheap deep equal comparison
if (JSON.stringify(project.extensions.i18n) !== JSON.stringify(i18n)) {
project.extensions.i18n = i18n;
await core.workspaces.writeWorkspace(workspace, this._host);
}
}
projectRelativePath(target) {
return core.relative(this._workspaceRoot, core.normalize(this._targetPathBuilder.createPath(target)));
}
async _readProjectI18n() {
const { project } = await this._readProject();
return project.extensions.i18n || {};
}
async _readProject() {
const { workspace } = await core.workspaces.readWorkspace(this._workspaceRoot, this._host);
const project = workspace.projects.get(this._projectName);
return { workspace, project };
}
_i18nLocale(target, locale) {
const translationPath = this.projectRelativePath(target);
const normalizedLocale = locale ? this._normalizeI18nLocale(locale) : { translation: [] };
if (!normalizedLocale.translation.includes(translationPath)) {
normalizedLocale.translation.push(translationPath);
}
const translation = normalizedLocale.translation.length === 1
? normalizedLocale.translation[0]
: normalizedLocale.translation;
return target.baseHref && target.baseHref !== target.language
? { translation, baseHref: target.baseHref }
: translation;
}
_normalizeI18nLocale(locale) {
if (typeof locale === 'string') {
return { translation: [locale] };
}
else if (Array.isArray(locale)) {
return { translation: locale };
}
else if (!Array.isArray(locale.translation)) {
return { ...locale, translation: [locale.translation] };
}
else {
return locale;
}
}
}
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol */
function __decorate(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
function __metadata(metadataKey, metadataValue) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue);
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
exports.AngularJsonPersistenceStrategy = class AngularJsonPersistenceStrategy extends server.PersistenceStrategy {
constructor(_i18n, _logger, _serializationStrategy) {
super();
this._i18n = _i18n;
this._logger = _logger;
this._serializationStrategy = _serializationStrategy;
}
async create(target) {
await this._write(target);
this._logger.info(`${server.timestamp()}: Created translation file for ${target.language} at ${this._i18n.projectRelativePath(target)}`);
}
async update(target) {
await this._write(target);
this._logger.info(`${server.timestamp()}: Updated translation file for ${target.language} at ${this._i18n.projectRelativePath(target)}`);
}
async _write(target) {
const filePath = this._i18n.projectRelativePath(target);
await this._serializationStrategy.serializeTarget(target, filePath);
await this._updateProjectI18n();
}
async _updateProjectI18n() {
await this._i18n.update();
}
};
exports.AngularJsonPersistenceStrategy = __decorate([
common.Injectable(),
__metadata("design:paramtypes", [AngularI18n, core.logging.Logger, server.SerializationStrategy])
], exports.AngularJsonPersistenceStrategy);
var index = architect.createBuilder(t9n);
function extractI18n() {
return new Promise((resolve, reject) => {
child_process.exec('npx nx extract-i18n --skip-nx-cache', (err) => {
if (!err) {
resolve();
}
reject(err);
});
});
}
async function t9n(options, context) {
var _a;
if (!context.target) {
throw new Error('To run this builder context.target is required!');
}
const nodeHost = new node.NodeJsSyncHost();
const host = core.workspaces.createWorkspaceHost(nodeHost);
const workspaceRoot = core.normalize(context.workspaceRoot);
const sourceFile = core.join(workspaceRoot, options.translationFile);
const autoTargetFile = options.autoTargetFile
? core.join(workspaceRoot, options.autoTargetFile)
: '';
const targetTranslationPath = options.targetTranslationPath || path.dirname(options.translationFile);
const targetDirectory = core.join(workspaceRoot, targetTranslationPath);
context.logger.info('otus-translation');
context.logger.info('===========');
context.logger.info(` - workspace root: ${workspaceRoot}`);
context.logger.info(` - source file: ${sourceFile}`);
context.logger.info(` - target directory: ${targetDirectory}`);
context.logger.info('');
if (!(await host.isFile(sourceFile))) {
return { success: false, error: `${options.translationFile} does not exist or is not a file!` };
}
else if (!(await host.isDirectory(targetDirectory))) {
return {
success: false,
error: `targetTranslationPath ${targetTranslationPath} is not a valid directory!`,
};
}
try {
await extractI18n();
}
catch (error) {
console.log(error);
}
const xliffVersion = await detectXliffVersion();
context.logger.info(`Detected version ${xliffVersion} of XLIFF`);
const targetPathBuilder = new server.TargetPathBuilder(targetDirectory, sourceFile);
let translationContext = null;
const angularI18n = new AngularI18n(host, workspaceRoot, context.target.project, targetPathBuilder, () => translationContext);
const sourceLocale = await angularI18n.sourceLocale();
context.logger.info(`Loading translations. Depending on the amount, this might take a moment.`);
const app = await core$1.NestFactory.create(server.AppModule.forRoot([
{ provide: core.logging.Logger, useValue: context.logger.createChild('NestJS') },
{ provide: server.WorkspaceHost, useValue: host },
{
provide: server.TargetInfo,
useValue: new server.TargetInfo(context.target.project, options.translationFile, sourceLocale.code, autoTargetFile),
},
{ provide: server.SerializationOptions, useValue: options },
{ provide: server.TargetPathBuilder, useValue: targetPathBuilder },
{ provide: AngularI18n, useValue: angularI18n },
{
provide: server.TranslationDeserializer,
useExisting: xliffVersion === '1.2' ? server.XlfDeserializer : server.Xlf2Deserializer,
},
{
provide: server.TranslationSerializer,
useExisting: xliffVersion === '1.2' ? server.XlfSerializer : server.Xlf2Serializer,
},
{
provide: server.TranslationSource,
useFactory: TRANSLATION_SOURCE_FACTORY,
inject: [server.SerializationStrategy],
},
{
provide: server.TranslationTargetRegistry,
useFactory: TRANSLATION_TARGET_REGISTRY_FACTORY,
inject: [server.TranslationSource, server.SerializationStrategy, server.PersistenceStrategy],
},
{ provide: server.PersistenceStrategy, useClass: exports.AngularJsonPersistenceStrategy },
]), {
cors: true,
logger: ['error', 'warn'],
});
app.setGlobalPrefix('api');
app.useWebSocketAdapter(new platformWs.WsAdapter(app));
app.useGlobalPipes(new common.ValidationPipe({ skipMissingProperties: true, whitelist: true }));
await app.listen((_a = options.port) !== null && _a !== void 0 ? _a : 4300, () => context.logger.info(`Translation server started: http://localhost:${options.port}\n`));
return new Promise(() => { });
async function detectXliffVersion() {
const content = await host.readFile(sourceFile);
const doc = new server.XmlParser().parse(content);
const version = doc.documentElement.getAttribute('version');
if (doc.documentElement.tagName !== 'xliff') {
throw new Error('Only xliff is supported!');
}
else if (version !== '2.0' && version !== '1.2') {
throw new Error('Unsupported xliff version!');
}
else {
return version;
}
}
async function TRANSLATION_SOURCE_FACTORY(serializationStrategy) {
try {
context.logger.info(`Attempting to serialize source file ${sourceFile}`);
const result = await serializationStrategy.deserializeSource(sourceFile);
if (result.language && sourceLocale.code && result.language !== sourceLocale.code) {
context.logger.warn(`Source locale in angular.json is ${sourceLocale} but in the ` +
` source file ${sourceFile} it is ${result.language}.`);
}
const source = new server.TranslationSource(sourceLocale.code || result.language, result.unitMap);
if (sourceLocale.baseHref) {
source.baseHref = sourceLocale.baseHref;
}
context.logger.info(`Successfully serialized source file ${sourceFile}`);
return source;
}
catch (e) {
context.logger.error(`Failed to serialize source file ${sourceFile}`);
throw e;
}
}
async function TRANSLATION_TARGET_REGISTRY_FACTORY(source, serializationStrategy, persistenceStrategy) {
try {
context.logger.info(`Attempting to serialize target files`);
const targetRegistry = new server.TranslationTargetRegistry(source, persistenceStrategy);
translationContext = { source, targetRegistry };
const locales = await angularI18n.locales();
await Promise.all(Object.keys(locales).map(async (language) => {
const locale = locales[language];
const normalizedPath = core.normalize(targetPathBuilder.createPath(language));
const relativePath = core.relative(workspaceRoot, normalizedPath);
if (locale.translation.every((t) => core.join(workspaceRoot, t) !== normalizedPath)) {
context.logger.warn(`Expected translation file ${relativePath} not found listed in i18n! It will be created and added to the i18n entry.`);
const target = await targetRegistry.create(language, locale.baseHref);
await importExistingTranslationUnits(target, locale.translation, serializationStrategy);
}
else if (!host.isFile(normalizedPath)) {
context.logger.warn(`Expected translation file ${relativePath} does not exist! It will be created.`);
await targetRegistry.create(language, locale.baseHref);
}
else {
const result = await serializationStrategy.deserializeTarget(normalizedPath);
targetRegistry.register(result.language, result.unitMap, locale.baseHref);
}
}));
// await angularI18n.update();
context.logger.info(`Successfully serialized target files`);
return targetRegistry;
}
catch (e) {
context.logger.error(`Failed to serialize target files`);
throw e;
}
}
async function importExistingTranslationUnits(target, translationFiles, serializationStrategy) {
for (const translation of translationFiles) {
const targetPath = core.join(workspaceRoot, translation);
const result = await serializationStrategy.deserializeTarget(targetPath);
result.unitMap.forEach((unit, key) => {
const targetUnit = target.unitMap.get(key);
if (targetUnit) {
target.translateUnit(targetUnit, unit);
}
});
}
}
}
exports.AngularI18n = AngularI18n;
exports.default = index;
exports.t9n = t9n;
Object.keys(server).forEach(function (k) {
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
enumerable: true,
get: function () { return server[k]; }
});
});