UNPKG

@o3r/localization

Version:

This module provides a runtime dynamic language/translation support and debug tools.

365 lines • 19.5 kB
"use strict"; 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