Skip to content

Commit

Permalink
feat: support multiple translation files in the i18n section of angul…
Browse files Browse the repository at this point in the history
…ar.json

The Angular CLI supports multiple translation files for the same locale.
angular-t9n only supported a single translation file, which this change
fixes, by being more flexible with the interpretation of the i18n section
in the angular.json.
  • Loading branch information
kyubisation committed Oct 25, 2020
1 parent e09b65e commit 13cedfc
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 120 deletions.
50 changes: 33 additions & 17 deletions builders/t9n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
XlfDeserializer,
XmlParser,
} from './deserialization';
import { TranslationSource } from './models';
import { TranslationSource, TranslationTarget } from './models';
import {
AngularI18n,
AngularJsonPersistenceStrategy,
Expand Down Expand Up @@ -173,23 +173,22 @@ export async function t9n(options: Options, context: BuilderContext): Promise<Bu
await Promise.all(
Object.keys(locales).map(async (language) => {
const locale = locales[language];
const targetPath = join(workspaceRoot, locale.translation);
if (host.isFile(targetPath)) {
const result = await serializationStrategy.deserializeTarget(targetPath);
const target = targetRegistry.register(result.language, result.unitMap, locale.baseHref);

const normalizedPath = targetPathBuilder.createPath(target);
if (targetPath !== normalizedPath) {
context.logger.info(
`Normalizing path for ${target.language}\n => Moving ${relative(
workspaceRoot,
targetPath
)} to ${relative(workspaceRoot, normalizedPath)}`
);
await nodeHost.rename(targetPath, normalizedPath).toPromise();
}
} else {
const normalizedPath = 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);
}
})
);
Expand All @@ -198,4 +197,21 @@ export async function t9n(options: Options, context: BuilderContext): Promise<Bu
await angularI18n.update();
return targetRegistry;
}

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);
}
});
}
}
}
6 changes: 0 additions & 6 deletions builders/t9n/persistence/angular-json-i18n.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { join, normalize, relative, virtualFs, workspaces } from '@angular-devkit/core';

import { TranslationSource, TranslationTarget } from '../models';
import { TranslationSource, TranslationTarget } from '../../models';
import { TargetPathBuilder } from '../target-path-builder';
import { TranslationTargetRegistry } from '../translation-target-registry';

import { AngularI18n } from './angular-i18n';
import { TargetPathBuilder } from './target-path-builder';
import { TranslationTargetRegistry } from './translation-target-registry';

describe('AngularI18n', () => {
const workspaceRoot = normalize(__dirname);
Expand All @@ -27,24 +27,29 @@ describe('AngularI18n', () => {
translationContext = null!;
});

async function setupI18n(i18n: any) {
const angularJson = require('../../../../angular.json');
angularJson.projects[projectName].i18n = i18n;
await host.writeFile(angularJsonPath, JSON.stringify(angularJson));
angularI18n = new AngularI18n(
host,
workspaceRoot,
projectName,
builder,
() => translationContext
);
}

describe('with i18n configured', () => {
beforeEach(async () => {
const angularJson = require('../../../angular.json');
angularJson.projects[projectName].i18n = {
sourceLocale: 'en',
locales: {
de: 'src/locale/xlf2/messages.de.xlf',
},
};
await host.writeFile(angularJsonPath, JSON.stringify(angularJson));
angularI18n = new AngularI18n(
host,
workspaceRoot,
projectName,
builder,
() => translationContext
);
});
beforeEach(
async () =>
await setupI18n({
sourceLocale: 'en',
locales: {
de: 'src/locale/xlf2/messages.de.xlf',
},
})
);

it('should throw without source', () => {
expect(angularI18n.update()).rejects.toThrow();
Expand All @@ -60,7 +65,7 @@ describe('AngularI18n', () => {
const locales = await angularI18n.locales();
expect(Object.keys(locales)).toEqual(['de']);
const deLocale = locales.de;
expect(deLocale.translation).toEqual('src/locale/xlf2/messages.de.xlf');
expect(deLocale.translation).toEqual(['src/locale/xlf2/messages.de.xlf']);
expect(deLocale.baseHref).toBeUndefined();
});

Expand All @@ -76,45 +81,68 @@ describe('AngularI18n', () => {
};
await angularI18n.update();
const ngJson = JSON.parse(await host.readFile(angularJsonPath));
const dePath = relative(
workspaceRoot,
builder.createPath({ language: 'de' } as TranslationTarget)
);
const deChPath = relative(
workspaceRoot,
builder.createPath({ language: 'de-CH' } as TranslationTarget)
);
const dePath = relative(workspaceRoot, builder.createPath('de'));
const deChPath = relative(workspaceRoot, builder.createPath('de-CH'));
expect(ngJson.projects[projectName].i18n).toEqual({
sourceLocale: 'en-US',
locales: { de: dePath, 'de-CH': deChPath },
locales: { de: ['src/locale/xlf2/messages.de.xlf', dePath], 'de-CH': deChPath },
});
});
});

describe('with i18n configured with baseHref', () => {
beforeEach(async () => {
const angularJson = require('../../../angular.json');
angularJson.projects[projectName].i18n = {
describe('with i18n configured without locales', () => {
beforeEach(
async () =>
await setupI18n({
sourceLocale: {
baseHref: '/en/',
language: 'en-US',
},
})
);

it('should update the angular.json when changed', async () => {
translationContext = {
source: { language: 'en-US' } as TranslationSource,
targetRegistry: {
values: () => [{ language: 'de' } as TranslationTarget],
} as any,
};
await angularI18n.update();
const ngJson = JSON.parse(await host.readFile(angularJsonPath));
const dePath = relative(workspaceRoot, builder.createPath('de'));
expect(ngJson.projects[projectName].i18n).toEqual({
sourceLocale: {
code: 'en',
baseHref: '/en/',
code: 'en-US',
},
locales: {
de: {
translation: 'src/locale/xlf2/messages.de.xlf',
baseHref: '/de/',
},
},
};
await host.writeFile(angularJsonPath, JSON.stringify(angularJson));
angularI18n = new AngularI18n(
host,
workspaceRoot,
projectName,
builder,
() => translationContext
);
locales: { de: dePath },
});
});
});

describe('with i18n configured with baseHref', () => {
beforeEach(
async () =>
await setupI18n({
sourceLocale: {
code: 'en',
baseHref: '/en/',
},
locales: {
de: {
translation: 'locales/xlf2/messages.de.xlf',
baseHref: '/de/',
},
'de-CH': {
translation: ['locales/xlf2/messages.de-CH.xlf', 'locales/xlf2/messages2.de-CH.xlf'],
baseHref: '/de-CH/',
},
fr: 'locales/xlf2/messages.fr.xlf',
'fr-CH': ['locales/xlf2/messages.fr-CH.xlf', 'locales/xlf2/messages2.fr-CH.xlf'],
},
})
);

it('should return the source locale', async () => {
const sourceLocale = await angularI18n.sourceLocale();
Expand All @@ -124,9 +152,9 @@ describe('AngularI18n', () => {

it('should return the target locales', async () => {
const locales = await angularI18n.locales();
expect(Object.keys(locales)).toEqual(['de']);
expect(Object.keys(locales)).toEqual(['de', 'de-CH', 'fr', 'fr-CH']);
const deLocale = locales.de;
expect(deLocale.translation).toEqual('src/locale/xlf2/messages.de.xlf');
expect(deLocale.translation).toEqual(['locales/xlf2/messages.de.xlf']);
expect(deLocale.baseHref).toEqual('/de/');
});

Expand All @@ -142,26 +170,24 @@ describe('AngularI18n', () => {
};
await angularI18n.update();
const ngJson = JSON.parse(await host.readFile(angularJsonPath));
const dePath = relative(
workspaceRoot,
builder.createPath({ language: 'de' } as TranslationTarget)
);
const deChPath = relative(
workspaceRoot,
builder.createPath({ language: 'de-CH' } as TranslationTarget)
);
const dePath = relative(workspaceRoot, builder.createPath('de'));
const deChPath = relative(workspaceRoot, builder.createPath('de-CH'));
expect(ngJson.projects[projectName].i18n).toEqual({
sourceLocale: {
code: 'en-US',
baseHref: '/en-US/',
},
locales: {
de: {
translation: dePath,
translation: ['locales/xlf2/messages.de.xlf', dePath],
baseHref: '/de/',
},
'de-CH': {
translation: deChPath,
translation: [
'locales/xlf2/messages.de-CH.xlf',
'locales/xlf2/messages2.de-CH.xlf',
deChPath,
],
baseHref: '/de-CH/',
},
},
Expand All @@ -170,18 +196,7 @@ describe('AngularI18n', () => {
});

describe('without i18n configured', () => {
beforeEach(async () => {
const angularJson = require('../../../angular.json');
angularJson.projects[projectName].i18n = undefined;
await host.writeFile(angularJsonPath, JSON.stringify(angularJson));
angularI18n = new AngularI18n(
host,
workspaceRoot,
projectName,
builder,
() => translationContext
);
});
beforeEach(async () => await setupI18n(undefined));

it('should return undefined for the source locale', async () => {
const sourceLocale = await angularI18n.sourceLocale();
Expand Down
Loading

0 comments on commit 13cedfc

Please sign in to comment.