@o3r/localization
Version:
This module provides a runtime dynamic language/translation support and debug tools.
365 lines • 19.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateI18n = void 0;
exports.updateLocalization = updateLocalization;
const path = require("node:path");
const schematics_1 = require("@angular-devkit/schematics");
const schematics_2 = require("@o3r/schematics");
const utility_1 = require("@schematics/angular/utility");
const ast_utils_1 = require("@schematics/angular/utility/ast-utils");
const change_1 = require("@schematics/angular/utility/change");
const ts = require("typescript");
/**
* Add Otter localization support
* @param options @see RuleFactory.options
* @param options.projectName
* @param rootPath @see RuleFactory.rootPath
*/
function updateLocalization(options, rootPath) {
const mainAssetsFolder = 'src/assets';
const devResourcesFolder = 'dev-resources';
/**
* Generate locales folder
* @param tree
*/
const generateLocalesFolder = (tree) => {
const workspaceProject = options.projectName ? (0, schematics_2.getWorkspaceConfig)(tree)?.projects[options.projectName] : undefined;
const projectType = workspaceProject?.projectType || 'application';
if (projectType === 'library') {
return tree;
}
const workingDirectory = (options.projectName && (0, schematics_2.getWorkspaceConfig)(tree)?.projects[options.projectName]?.root) || '.';
return (0, schematics_1.mergeWith)((0, schematics_1.apply)((0, schematics_1.url)((0, schematics_2.getTemplateFolder)(rootPath, __dirname)), [
(0, schematics_1.template)({ empty: '' }),
(0, schematics_1.move)(workingDirectory)
]), schematics_1.MergeStrategy.Overwrite);
};
/**
* Add translation generation builders into angular.json
* @param tree
* @param context
*/
const updateAngularJson = (tree, context) => {
const workspace = (0, schematics_2.getWorkspaceConfig)(tree);
const projectName = options.projectName;
const workspaceProject = options.projectName ? workspace?.projects[options.projectName] : undefined;
const projectRoot = path.posix.join(workspaceProject?.root || '');
const distFolder = (workspaceProject
&& workspaceProject.architect
&& workspaceProject.architect.build
&& workspaceProject.architect.build.options
&& ((workspaceProject.architect.build.configurations
&& workspaceProject.architect.build.configurations.production
&& workspaceProject.architect.build.configurations.production.outputPath) || workspaceProject.architect.build.options.outputPath)) || './dist';
// exit if not an application
if (!workspace || !projectName || !workspaceProject || workspaceProject.projectType === 'library') {
context.logger.debug('No application project found to add translation extraction');
return tree;
}
if (!workspaceProject.architect) {
workspaceProject.architect = {};
}
const projectBasePath = workspaceProject.root.replace(/[/\\]$/, '');
workspaceProject.architect['generate-translations'] ||= {
builder: '@o3r/localization:localization',
options: {
browserTarget: `${projectName}:build`,
localizationExtracterTarget: `${projectName}:extract-translations`,
locales: [
'en-GB'
],
assets: [`${projectBasePath}/${mainAssetsFolder}/locales`],
outputPath: `${projectBasePath}/${devResourcesFolder}/localizations`
},
configurations: {
production: {
outputPath: `${projectBasePath}/${distFolder}/localizations`
}
}
};
const pathTsconfigCms = path.posix.join(projectRoot, 'tsconfig.cms.json');
workspaceProject.architect['extract-translations'] ||= {
builder: '@o3r/localization:extractor',
options: {
tsConfig: pathTsconfigCms.replace(/^\//, ''),
libraries: []
}
};
const localizationAssetsConfig = {
glob: '**/*.json',
input: `${projectBasePath}/${devResourcesFolder}/localizations`,
output: '/localizations'
};
const projectType = workspaceProject?.projectType || 'application';
if (projectType === 'application' && workspaceProject.architect.build) {
const alreadyExistingBuildOption = workspaceProject.architect.build.options?.assets
?.map((a) => a.output)
.find((output) => output === '/localizations');
if (!alreadyExistingBuildOption) {
workspaceProject.architect.build.options ||= {};
workspaceProject.architect.build.options.assets ||= [];
workspaceProject.architect.build.options.assets.push(localizationAssetsConfig);
}
}
if (workspaceProject.architect.test) {
const alreadyExistingTestOption = workspaceProject.architect.test.options?.assets
?.map((a) => a.output)
.find((output) => output === '/localizations');
if (!alreadyExistingTestOption) {
workspaceProject.architect.test.options ||= {};
workspaceProject.architect.test.options.assets ||= [];
workspaceProject.architect.test.options.assets.push(localizationAssetsConfig);
}
}
const targets = [
`${projectName}:generate-translations`,
`${projectName}:serve`
];
if (workspaceProject.architect.run && workspaceProject.architect.run.options && Array.isArray(workspaceProject.architect.run.options.targets)) {
workspaceProject.architect.run.options.targets.push(...targets);
workspaceProject.architect.run.options.targets = workspaceProject.architect.run.options.targets
.reduce((acc, target) => acc.includes(target) ? acc : [...acc, target], []);
}
else {
workspaceProject.architect.run = {
builder: '@o3r/core:multi-watcher',
options: {
targets
}
};
}
workspace.projects[projectName] = workspaceProject;
return (0, schematics_2.writeAngularJson)(tree, workspace);
};
/**
* Changed package.json start script to run localization generation
* @param tree
* @param context
*/
const updatePackageJson = (tree, context) => {
const workspace = (0, schematics_2.getWorkspaceConfig)(tree);
const projectName = options.projectName;
const workspaceProject = options.projectName ? workspace?.projects[options.projectName] : undefined;
const packageManagerRunner = (0, schematics_2.getPackageManagerRunner)((0, schematics_2.getWorkspaceConfig)(tree));
if (!projectName || !workspace || !workspaceProject || workspaceProject.projectType === 'library') {
context.logger.debug('No application project found to add translation extraction');
return tree;
}
const packageJson = (0, schematics_2.readPackageJson)(tree, workspaceProject);
packageJson.scripts = packageJson.scripts || {};
if (packageJson.scripts && packageJson.scripts.start && packageJson.scripts.start !== `ng run ${projectName}:run`) {
packageJson.scripts['start:no-translation'] ||= packageJson.scripts.start;
}
if (packageJson.scripts.watch) {
delete packageJson.scripts.watch;
}
packageJson.scripts.start ||= `ng run ${projectName}:run`;
if (packageJson.scripts.build?.indexOf('generate:translations') === -1) {
packageJson.scripts.build = `${packageManagerRunner} generate:translations && ${packageJson.scripts.build}`;
}
packageJson.scripts['generate:translations:dev'] ||= `ng run ${projectName}:generate-translations`;
packageJson.scripts['generate:translations'] ||= `ng run ${projectName}:generate-translations:production`;
tree.overwrite(`${workspaceProject.root}/package.json`, JSON.stringify(packageJson, null, 2));
};
/**
* Edit main module with the translation required configuration
* @param tree
* @param context
*/
const registerModules = (tree, context) => {
const additionalRules = [];
const moduleFilePath = (0, schematics_2.getAppModuleFilePath)(tree, context, options.projectName);
if (!moduleFilePath || !tree.exists(moduleFilePath)) {
context.logger.warn(moduleFilePath
? `Module file not found under '${moduleFilePath}'. Localization modules not registered.`
: 'No module file found. Localization modules not registered.');
return tree;
}
const sourceFile = ts.createSourceFile(moduleFilePath, tree.read(moduleFilePath).toString(), ts.ScriptTarget.ES2015, true);
// avoid overriding app module if Localization module is already imported
if ((0, ast_utils_1.isImported)(sourceFile, 'LocalizationModule', '@otter/common') || (0, ast_utils_1.isImported)(sourceFile, 'LocalizationModule', '@o3r/localization')) {
return tree;
}
const recorder = tree.beginUpdate(moduleFilePath);
const appModuleFile = tree.read(moduleFilePath).toString();
const { moduleIndex } = (0, schematics_2.getModuleIndex)(sourceFile, appModuleFile);
const addImportToModuleFile = (name, file, moduleFunction) => additionalRules.push((0, utility_1.addRootImport)(options.projectName, ({ code, external }) => code `${external(name, file)}${moduleFunction}`));
const insertImportToModuleFile = (name, file, isDefault) => (0, schematics_2.insertImportToModuleFile)(name, file, sourceFile, recorder, moduleFilePath, isDefault);
const addProviderToModuleFile = (name, file, customProvider) => additionalRules.push((0, utility_1.addRootProvider)(options.projectName, ({ code, external }) => code `{provide: ${external(name, file)}, ${customProvider}}`));
const insertBeforeModule = (line) => (0, schematics_2.insertBeforeModule)(line, appModuleFile, recorder, moduleIndex);
addImportToModuleFile('TranslateModule', '@ngx-translate/core', `.forRoot({
loader: translateLoaderProvider,
compiler: {
provide: TranslateCompiler,
useClass: TranslateMessageFormatLazyCompiler
}
})`);
addImportToModuleFile('LocalizationModule', '@o3r/localization', '.forRoot(localizationConfigurationFactory)');
insertImportToModuleFile('TranslateCompiler', '@ngx-translate/core');
insertImportToModuleFile('translateLoaderProvider', '@o3r/localization');
insertImportToModuleFile('TranslateMessageFormatLazyCompiler', '@o3r/localization');
insertImportToModuleFile('LocalizationConfiguration', '@o3r/localization');
insertImportToModuleFile('registerLocaleData', '@angular/common');
insertImportToModuleFile('localeEN', '@angular/common/locales/en', true);
insertBeforeModule('registerLocaleData(localeEN, \'en-GB\');');
insertBeforeModule(`export function localizationConfigurationFactory(): Partial<LocalizationConfiguration> {
return {
supportedLocales: ['en-GB'],
fallbackLanguage: 'en-GB',
bundlesOutputPath: 'localizations/', // TODO? get it from a property
useDynamicContent: environment.production
};
}`);
addProviderToModuleFile('MESSAGE_FORMAT_CONFIG', '@o3r/localization', 'useValue: {}');
tree.commitUpdate(recorder);
if (!(0, ast_utils_1.isImported)(sourceFile, 'environment', '../environments/environment')) {
insertImportToModuleFile('environment', '../environments/environment');
}
return (0, schematics_1.chain)(additionalRules)(tree, context);
};
/**
* Set language as default on application bootstrap
* @param tree
* @param context
*/
const setDefaultLanguage = (tree, context) => {
const moduleFilePath = (0, schematics_2.getAppModuleFilePath)(tree, context, options.projectName);
const componentFilePath = moduleFilePath && moduleFilePath.replace(/\.(?:module|config)\.ts$/i, '.component.ts');
if (!(componentFilePath && tree.exists(componentFilePath))) {
context.logger.warn(`File ${componentFilePath} not found, the default language won't be set`);
return tree;
}
const sourceFile = ts.createSourceFile(componentFilePath, tree.read(componentFilePath).toString(), ts.ScriptTarget.ES2015, true);
// avoid overriding app component file if Localization service is already imported
if ((0, ast_utils_1.isImported)(sourceFile, 'LocalizationService', '@otter/common') || (0, ast_utils_1.isImported)(sourceFile, 'LocalizationService', '@o3r/localization')) {
return tree;
}
const recorder = tree.beginUpdate(componentFilePath);
const insertImportToComponentFile = (name, file, isDefault) => {
const importChange = (0, ast_utils_1.insertImport)(sourceFile, componentFilePath, name, file, isDefault);
if (importChange instanceof change_1.InsertChange) {
recorder.insertLeft(importChange.pos, importChange.toAdd);
}
};
insertImportToComponentFile('LocalizationService', '@o3r/localization');
const constructorNode = (0, schematics_2.findFirstNodeOfKind)(sourceFile, ts.SyntaxKind.Constructor);
if (constructorNode) {
const hasParam = !!constructorNode.parameters && constructorNode.parameters.length > 0;
const pos = hasParam ? constructorNode.parameters[0].pos : constructorNode.pos + constructorNode.getText().indexOf('(') + 1;
recorder.insertLeft(pos, `localizationService: LocalizationService${hasParam ? ', ' : ''}`);
if (constructorNode.body) {
recorder.insertLeft(constructorNode.body.end - 1, 'localizationService.useLanguage(\'en-GB\');');
}
}
else {
const classNode = (0, schematics_2.findFirstNodeOfKind)(sourceFile, ts.SyntaxKind.ClassDeclaration);
if (classNode) {
const firstToken = classNode.members[0];
if (firstToken) {
recorder.insertLeft(firstToken.pos, '\n constructor(localizationService: LocalizationService) { localizationService.useLanguage(\'en-GB\'); }');
}
}
else {
context.logger.warn(`No class found in ${componentFilePath}, the default language won't be set`);
}
}
tree.commitUpdate(recorder);
return tree;
};
/**
* Add mockTranslationModule to application tests.
* @param tree
* @param context
*/
const addMockTranslationModule = (tree, context) => {
const moduleFilePath = (0, schematics_2.getAppModuleFilePath)(tree, context, options.projectName);
const possibleComponentSpecPaths = [
moduleFilePath && moduleFilePath.replace(/\.(?:module|config)\.ts$/i, '.component.spec.ts'),
moduleFilePath && moduleFilePath.replace(/\.(?:module|config)\.ts$/i, '.spec.ts')
];
const componentSpecFilePath = possibleComponentSpecPaths.find((compPath) => compPath && tree.exists(compPath));
if (!(componentSpecFilePath && tree.exists(componentSpecFilePath))) {
return tree;
}
if (!(componentSpecFilePath && tree.exists(componentSpecFilePath))) {
return tree;
}
const sourceFile = ts.createSourceFile(componentSpecFilePath, tree.read(componentSpecFilePath).toString(), ts.ScriptTarget.ES2015, true);
// avoid overriding app component spec file if Localization mock is already imported
if ((0, ast_utils_1.isImported)(sourceFile, 'mockTranslationModules', '@otter/common') || (0, ast_utils_1.isImported)(sourceFile, 'mockTranslationModules', '@o3r/testing/localization')) {
return tree;
}
const recorder = tree.beginUpdate(componentSpecFilePath);
const insertImportToComponentFile = (name, file, isDefault) => {
const importChange = (0, ast_utils_1.insertImport)(sourceFile, componentSpecFilePath, name, file, isDefault);
if (importChange instanceof change_1.InsertChange) {
recorder.insertLeft(importChange.pos, importChange.toAdd);
}
};
insertImportToComponentFile('mockTranslationModules', '@o3r/testing/localization');
const regExp = /TestBed\.configureTestingModule\({.*imports\s*:\s*\[(\s*)/s;
const result = sourceFile.text.match(regExp);
if (result && result.length > 0 && typeof result.index !== 'undefined') {
recorder.insertRight(result.index + result[0].length, '...mockTranslationModules(),' + result[1]);
}
tree.commitUpdate(recorder);
return tree;
};
// Ignore generated CMS metadata
const ignoreDevResourcesFiles = (tree, _context) => {
const workingDirectory = (options.projectName && (0, schematics_2.getWorkspaceConfig)(tree)?.projects[options.projectName]?.root) || '.';
return (0, schematics_1.applyToSubtree)(workingDirectory, [
(subTree) => (0, schematics_2.ignorePatterns)(subTree, [
{ description: 'Local Development resources files', patterns: [`/${devResourcesFolder}`] },
{ description: 'CMS metadata files', patterns: ['/*.metadata.json'] }
])
]);
};
return (0, schematics_1.chain)([
registerModules,
generateLocalesFolder,
updateAngularJson,
updatePackageJson,
setDefaultLanguage,
addMockTranslationModule,
ignoreDevResourcesFiles
]);
}
function updateI18nFn(options) {
if (!options.projectName) {
return schematics_1.noop;
}
/**
* Add i18n generation builders into angular.json
* @param tree
*/
const updateAngularJson = (tree) => {
const workspace = (0, schematics_2.getWorkspaceConfig)(tree);
const workspaceProject = options.projectName ? workspace?.projects[options.projectName] : undefined;
if (!workspace || !workspaceProject) {
return tree;
}
if (!workspaceProject.architect) {
workspaceProject.architect = {};
}
if (!workspaceProject.architect.i18n) {
workspaceProject.architect.i18n ||= {
builder: '@o3r/localization:i18n',
options: {
localizationConfigs: [{
localizationFiles: workspace.schematics && workspace.schematics['@o3r/core:component']
? [workspace.schematics['@o3r/core:component'].path]
: ['**/*.localization.json']
}]
}
};
}
workspace.projects[options.projectName] = workspaceProject;
return (0, schematics_2.writeAngularJson)(tree, workspace);
};
return (0, schematics_1.chain)([
updateAngularJson
]);
}
exports.updateI18n = (0, schematics_2.createOtterSchematic)(updateI18nFn);
//# sourceMappingURL=index.js.map