-
Notifications
You must be signed in to change notification settings - Fork 12k
feature(i18n): implement xi18n command #3340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
1eb2a87
feature(i18n): implement i18n command
cladera 878282e
Remove acceptance tests and create e2e tests
cladera 0bd69d1
Reject task if format argument is unknown
cladera a28da7d
Feature redone to use ngtools api
cladera f5de5be
Fix code after rebase master
cladera ccc7aed
Fix e2e tests after rebase master
cladera 3195183
Assert an AotPlugin instance is present in webpack compilation.
cladera 18adc30
Rebase master
cladera File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
const Command = require('../ember-cli/lib/models/command'); | ||
|
||
import {Extracti18nTask} from '../tasks/extract-i18n'; | ||
|
||
export interface Xi18nOptions { | ||
outputPath?: string; | ||
verbose?: boolean; | ||
i18nFormat?: string; | ||
} | ||
|
||
const Xi18nCommand = Command.extend({ | ||
name: 'xi18n', | ||
description: 'Extracts i18n messages from source code.', | ||
works: 'insideProject', | ||
availableOptions: [ | ||
{ | ||
name: 'i18n-format', | ||
type: String, | ||
default: 'xlf', | ||
aliases: ['f', {'xmb': 'xmb'}, {'xlf': 'xlf'}, {'xliff': 'xlf'}] | ||
}, | ||
{ name: 'output-path', type: 'Path', default: null, aliases: ['op']}, | ||
{ name: 'verbose', type: Boolean, default: false}, | ||
{ name: 'progress', type: Boolean, default: true } | ||
|
||
], | ||
run: function (commandOptions: any) { | ||
|
||
const xi18nTask = new Extracti18nTask({ | ||
ui: this.ui, | ||
project: this.project | ||
}); | ||
|
||
return xi18nTask.run(commandOptions); | ||
} | ||
}); | ||
|
||
|
||
export default Xi18nCommand; | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import * as path from 'path'; | ||
import {ExtractI18nPlugin} from '@ngtools/webpack'; | ||
|
||
export const getWebpackExtractI18nConfig = function( | ||
projectRoot: string, | ||
appConfig: any, | ||
genDir: string, | ||
i18nFormat: string): any { | ||
|
||
let exclude: string[] = []; | ||
if (appConfig.test) { | ||
exclude.push(path.join(projectRoot, appConfig.root, appConfig.test)); | ||
} | ||
|
||
return { | ||
plugins: [ | ||
new ExtractI18nPlugin({ | ||
tsConfigPath: path.resolve(projectRoot, appConfig.root, appConfig.tsconfig), | ||
exclude: exclude, | ||
genDir: genDir, | ||
i18nFormat: i18nFormat | ||
}) | ||
] | ||
}; | ||
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import * as path from 'path'; | ||
|
||
import {CliConfig} from './config'; | ||
import {NgCliWebpackConfig} from './webpack-config'; | ||
const webpackMerge = require('webpack-merge'); | ||
import {getWebpackExtractI18nConfig} from './webpack-configs'; | ||
|
||
export interface XI18WebpackOptions { | ||
genDir?: string; | ||
buildDir?: string; | ||
i18nFormat?: string; | ||
verbose?: boolean; | ||
progress?: boolean; | ||
} | ||
export class XI18nWebpackConfig extends NgCliWebpackConfig { | ||
|
||
public config: any; | ||
|
||
constructor(extractOptions: XI18WebpackOptions) { | ||
|
||
super({ | ||
target: 'development', | ||
verbose: extractOptions.verbose, | ||
progress: extractOptions.progress | ||
}); | ||
|
||
const configPath = CliConfig.configFilePath(); | ||
const projectRoot = path.dirname(configPath); | ||
const appConfig = CliConfig.fromProject().config.apps[0]; | ||
|
||
const extractI18nConfig = | ||
getWebpackExtractI18nConfig(projectRoot, | ||
appConfig, | ||
extractOptions.genDir, | ||
extractOptions.i18nFormat); | ||
|
||
this.config = webpackMerge([this.config, extractI18nConfig]); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import * as webpack from 'webpack'; | ||
import * as path from 'path'; | ||
import * as rimraf from 'rimraf'; | ||
|
||
const Task = require('../ember-cli/lib/models/task'); | ||
|
||
import {XI18nWebpackConfig} from '../models/webpack-xi18n-config'; | ||
import {CliConfig} from '../models/config'; | ||
|
||
|
||
export const Extracti18nTask = Task.extend({ | ||
run: function (runTaskOptions: any) { | ||
|
||
const project = this.project; | ||
|
||
const appConfig = CliConfig.fromProject().config.apps[0]; | ||
|
||
const buildDir = '.tmp'; | ||
const genDir = runTaskOptions.outputPath || appConfig.root; | ||
|
||
const config = new XI18nWebpackConfig({ | ||
genDir, | ||
buildDir, | ||
i18nFormat: runTaskOptions.i18nFormat, | ||
verbose: runTaskOptions.verbose, | ||
progress: runTaskOptions.progress | ||
}).config; | ||
|
||
const webpackCompiler = webpack(config); | ||
|
||
return new Promise((resolve, reject) => { | ||
const callback: webpack.compiler.CompilerCallback = (err, stats) => { | ||
if (err) { | ||
return reject(err); | ||
} | ||
|
||
if (stats.hasErrors()) { | ||
reject(); | ||
} else { | ||
resolve(); | ||
} | ||
}; | ||
|
||
webpackCompiler.run(callback); | ||
}) | ||
.then(() => { | ||
// Deletes temporary build folder | ||
rimraf.sync(path.resolve(project.root, buildDir)); | ||
}) | ||
.catch((err: Error) => { | ||
if (err) { | ||
this.ui.writeError('\nAn error occured during the i18n extraction:\n' | ||
+ ((err && err.stack) || err)); | ||
} | ||
throw err; | ||
}); | ||
} | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
import * as ts from 'typescript'; | ||
import * as path from 'path'; | ||
import * as fs from 'fs'; | ||
|
||
import {__NGTOOLS_PRIVATE_API_2} from '@angular/compiler-cli'; | ||
|
||
import {Tapable} from './webpack'; | ||
import {WebpackResourceLoader} from './resource_loader'; | ||
|
||
export interface ExtractI18nPluginOptions { | ||
tsConfigPath: string; | ||
basePath?: string; | ||
genDir?: string; | ||
i18nFormat?: string; | ||
exclude?: string[]; | ||
} | ||
|
||
export class ExtractI18nPlugin implements Tapable { | ||
private _resourceLoader: WebpackResourceLoader; | ||
|
||
private _donePromise: Promise<void>; | ||
private _compiler: any = null; | ||
private _compilation: any = null; | ||
|
||
private _tsConfigPath: string; | ||
private _basePath: string; | ||
private _genDir: string; | ||
private _rootFilePath: string[]; | ||
private _compilerOptions: any = null; | ||
private _angularCompilerOptions: any = null; | ||
// private _compilerHost: WebpackCompilerHost; | ||
private _compilerHost: ts.CompilerHost; | ||
private _program: ts.Program; | ||
|
||
private _i18nFormat: string; | ||
|
||
constructor(options: ExtractI18nPluginOptions) { | ||
this._setupOptions(options); | ||
} | ||
|
||
private _setupOptions(options: ExtractI18nPluginOptions) { | ||
if (!options.hasOwnProperty('tsConfigPath')) { | ||
throw new Error('Must specify "tsConfigPath" in the configuration of @ngtools/webpack.'); | ||
} | ||
this._tsConfigPath = options.tsConfigPath; | ||
|
||
// Check the base path. | ||
const maybeBasePath = path.resolve(process.cwd(), this._tsConfigPath); | ||
let basePath = maybeBasePath; | ||
if (fs.statSync(maybeBasePath).isFile()) { | ||
basePath = path.dirname(basePath); | ||
} | ||
if (options.hasOwnProperty('basePath')) { | ||
basePath = path.resolve(process.cwd(), options.basePath); | ||
} | ||
|
||
let tsConfigJson: any = null; | ||
try { | ||
tsConfigJson = JSON.parse(fs.readFileSync(this._tsConfigPath, 'utf8')); | ||
} catch (err) { | ||
throw new Error(`An error happened while parsing ${this._tsConfigPath} JSON: ${err}.`); | ||
} | ||
const tsConfig = ts.parseJsonConfigFileContent( | ||
tsConfigJson, ts.sys, basePath, null, this._tsConfigPath); | ||
|
||
let fileNames = tsConfig.fileNames; | ||
if (options.hasOwnProperty('exclude')) { | ||
let exclude: string[] = typeof options.exclude == 'string' | ||
? [options.exclude as string] : (options.exclude as string[]); | ||
|
||
exclude.forEach((pattern: string) => { | ||
const basePathPattern = '(' + basePath.replace(/\\/g, '/') | ||
.replace(/[\-\[\]\/{}()+?.\\^$|*]/g, '\\$&') + ')?'; | ||
pattern = pattern | ||
// Replace windows path separators with forward slashes. | ||
.replace(/\\/g, '/') | ||
// Escape characters that are used normally in regexes, except stars. | ||
.replace(/[\-\[\]{}()+?.\\^$|]/g, '\\$&') | ||
// Two stars replacement. | ||
.replace(/\*\*/g, '(?:.*)') | ||
// One star replacement. | ||
.replace(/\*/g, '(?:[^/]*)') | ||
// Escape characters from the basePath and make sure it's forward slashes. | ||
.replace(/^/, basePathPattern); | ||
|
||
const re = new RegExp('^' + pattern + '$'); | ||
fileNames = fileNames.filter(x => !x.replace(/\\/g, '/').match(re)); | ||
}); | ||
} else { | ||
fileNames = fileNames.filter(fileName => !/\.spec\.ts$/.test(fileName)); | ||
} | ||
this._rootFilePath = fileNames; | ||
|
||
// By default messages will be generated in basePath | ||
let genDir = basePath; | ||
|
||
if (options.hasOwnProperty('genDir')) { | ||
genDir = path.resolve(process.cwd(), options.genDir); | ||
} | ||
|
||
this._compilerOptions = tsConfig.options; | ||
this._angularCompilerOptions = Object.assign( | ||
{ genDir }, | ||
this._compilerOptions, | ||
tsConfig.raw['angularCompilerOptions'], | ||
{ basePath } | ||
); | ||
|
||
this._basePath = basePath; | ||
this._genDir = genDir; | ||
|
||
// this._compilerHost = new WebpackCompilerHost(this._compilerOptions, this._basePath); | ||
this._compilerHost = ts.createCompilerHost(this._compilerOptions, true); | ||
this._program = ts.createProgram( | ||
this._rootFilePath, this._compilerOptions, this._compilerHost); | ||
|
||
if (options.hasOwnProperty('i18nFormat')) { | ||
this._i18nFormat = options.i18nFormat; | ||
} | ||
} | ||
|
||
apply(compiler: any) { | ||
this._compiler = compiler; | ||
|
||
compiler.plugin('make', (compilation: any, cb: any) => this._make(compilation, cb)); | ||
|
||
compiler.plugin('after-emit', (compilation: any, cb: any) => { | ||
this._donePromise = null; | ||
this._compilation = null; | ||
compilation._ngToolsWebpackXi18nPluginInstance = null; | ||
cb(); | ||
}); | ||
} | ||
|
||
private _make(compilation: any, cb: (err?: any, request?: any) => void) { | ||
this._compilation = compilation; | ||
if (this._compilation._ngToolsWebpackXi18nPluginInstance) { | ||
return cb(new Error('An @ngtools/webpack xi18n plugin already exist for ' + | ||
'this compilation.')); | ||
} | ||
if (!this._compilation._ngToolsWebpackPluginInstance) { | ||
return cb(new Error('An @ngtools/webpack aot plugin does not exists ' + | ||
'for this compilation')); | ||
} | ||
|
||
this._compilation._ngToolsWebpackXi18nPluginInstance = this; | ||
|
||
this._resourceLoader = new WebpackResourceLoader(compilation); | ||
|
||
this._donePromise = Promise.resolve() | ||
.then(() => { | ||
return __NGTOOLS_PRIVATE_API_2.extractI18n({ | ||
basePath: this._basePath, | ||
compilerOptions: this._compilerOptions, | ||
program: this._program, | ||
host: this._compilerHost, | ||
angularCompilerOptions: this._angularCompilerOptions, | ||
i18nFormat: this._i18nFormat, | ||
|
||
readResource: (path: string) => this._resourceLoader.get(path) | ||
}); | ||
}) | ||
.then(() => cb(), (err: any) => { | ||
compilation.errors.push(err); | ||
cb(); | ||
}); | ||
|
||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import 'reflect-metadata'; | ||
|
||
export * from './plugin'; | ||
export * from './extract_i18n_plugin'; | ||
export {ngcLoader as default} from './loader'; | ||
export {PathsPlugin} from './paths-plugin'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import {join} from 'path'; | ||
import {ng} from '../../utils/process'; | ||
import { | ||
expectFileToExist, writeFile, | ||
expectFileToMatch | ||
} from '../../utils/fs'; | ||
|
||
|
||
export default function() { | ||
return ng('generate', 'component', 'i18n-test') | ||
.then(() => writeFile( | ||
join('src/app/i18n-test', 'i18n-test.component.html'), | ||
'<p i18n>Hello world</p>')) | ||
.then(() => ng('xi18n', '--no-progress')) | ||
.then(() => expectFileToExist(join('src', 'messages.xlf'))) | ||
.then(() => expectFileToMatch(join('src', 'messages.xlf'), /Hello world/)); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import {join} from 'path'; | ||
import {ng} from '../../utils/process'; | ||
import { | ||
expectFileToExist, writeFile, | ||
expectFileToMatch | ||
} from '../../utils/fs'; | ||
|
||
|
||
export default function() { | ||
return ng('generate', 'component', 'i18n-test') | ||
.then(() => writeFile( | ||
join('src/app/i18n-test', 'i18n-test.component.html'), | ||
'<p i18n>Hello world</p>')) | ||
.then(() => ng('xi18n', '--no-progress', '--output-path', 'src/locale')) | ||
.then(() => expectFileToExist(join('src', 'locale', 'messages.xlf'))) | ||
.then(() => expectFileToMatch(join('src', 'locale', 'messages.xlf'), /Hello world/)); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should also validate that we don't have a
_ngToolsWebpackPluginInstance
, since that would seem to be an error.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes pushed. Travis build failed though. I don't understand why it failed, can you take a look and let me know if I broke something.