UNPKG

angular-t9n

Version:

A translation tool for Angular i18n

229 lines (211 loc) 8.66 kB
import { dirname } from 'path'; import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; import { join, json, logging, normalize, relative, workspaces } from '@angular-devkit/core'; import { NodeJsSyncHost } from '@angular-devkit/core/node'; import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { WsAdapter } from '@nestjs/platform-ws'; import { AppModule, PersistenceStrategy, SerializationOptions, SerializationStrategy, TargetInfo, TargetPathBuilder, TranslationDeserializer, TranslationSerializer, TranslationSource, TranslationTarget, TranslationTargetRegistry, WorkspaceHost, Xlf2Deserializer, Xlf2Serializer, XlfDeserializer, XlfSerializer, XmlParser, } from '../../server'; import { AngularI18n, AngularJsonPersistenceStrategy } from './persistence'; import { Schema as Options } from './schema'; export * from '../../server'; export * from './persistence'; export { Schema as t9nOptions } from './schema'; export default createBuilder<Options & json.JsonObject, BuilderOutput>(t9n); export async function t9n(options: Options, context: BuilderContext): Promise<BuilderOutput> { if (!context.target) { throw new Error('To run this builder context.target is required!'); } const nodeHost = new NodeJsSyncHost(); const host = workspaces.createWorkspaceHost(nodeHost); const workspaceRoot = normalize(context.workspaceRoot); const sourceFile = join(workspaceRoot, options.translationFile); const targetTranslationPath = options.targetTranslationPath || dirname(options.translationFile); const targetDirectory = join(workspaceRoot, targetTranslationPath); context.logger.info('angular-t9n'); 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!`, }; } const xliffVersion = await detectXliffVersion(); context.logger.info(`Detected version ${xliffVersion} of XLIFF`); const targetPathBuilder = new TargetPathBuilder(targetDirectory, sourceFile); let translationContext: { source: TranslationSource; targetRegistry: TranslationTargetRegistry; } = 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 NestFactory.create( AppModule.forRoot([ { provide: logging.Logger, useValue: context.logger.createChild('NestJS') }, { provide: WorkspaceHost, useValue: host }, { provide: TargetInfo, useValue: new TargetInfo( context.target.project, options.translationFile, sourceLocale.code, ), }, { provide: SerializationOptions, useValue: options }, { provide: TargetPathBuilder, useValue: targetPathBuilder }, { provide: AngularI18n, useValue: angularI18n }, { provide: TranslationDeserializer, useExisting: xliffVersion === '1.2' ? XlfDeserializer : Xlf2Deserializer, }, { provide: TranslationSerializer, useExisting: xliffVersion === '1.2' ? XlfSerializer : Xlf2Serializer, }, { provide: TranslationSource, useFactory: TRANSLATION_SOURCE_FACTORY, inject: [SerializationStrategy], }, { provide: TranslationTargetRegistry, useFactory: TRANSLATION_TARGET_REGISTRY_FACTORY, inject: [TranslationSource, SerializationStrategy, PersistenceStrategy], }, { provide: PersistenceStrategy, useClass: AngularJsonPersistenceStrategy }, ]), { cors: true, logger: ['error', 'warn'], }, ); app.setGlobalPrefix('api'); app.useWebSocketAdapter(new WsAdapter(app)); app.useGlobalPipes(new ValidationPipe({ skipMissingProperties: true, whitelist: true })); await app.listen(options.port ?? 4300, () => context.logger.info(`Translation server started: http://localhost:${options.port}\n`), ); return new Promise(() => {}); async function detectXliffVersion(): Promise<'2.0' | '1.2'> { const content = await host.readFile(sourceFile); const doc = new 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: SerializationStrategy, ): Promise<TranslationSource> { 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 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: TranslationSource, serializationStrategy: SerializationStrategy, persistenceStrategy: PersistenceStrategy, ): Promise<TranslationTargetRegistry> { try { context.logger.info(`Attempting to serialize target files`); const targetRegistry = new 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 = normalize(targetPathBuilder.createPath(language)); const relativePath = relative(workspaceRoot, normalizedPath); if (locale.translation.every((t) => 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: TranslationTarget, translationFiles: string[], serializationStrategy: SerializationStrategy, ) { for (const translation of translationFiles) { const targetPath = 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); } }); } } }