@actra-development-oss/ng-i18n-aot-loader
Advanced tools
Comparing version 0.0.5 to 1.0.0
56
index.js
@@ -40,3 +40,3 @@ /** | ||
let query = loaderUtils.getOptions(context) || {}; | ||
let configKey = query.config || 'ng2I18nLoader'; | ||
let configKey = query.config || 'ngI18nAotLoader'; | ||
let config = context.options && context.options.hasOwnProperty(configKey) ? context.options[configKey] : {}; | ||
@@ -50,20 +50,20 @@ | ||
function random4Chars() { | ||
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); | ||
}; | ||
/** | ||
* Uniq ID generator for automatically inserted template references | ||
*/ | ||
function uniqId() { | ||
return (random4Chars() + random4Chars() + random4Chars() + random4Chars() + random4Chars() + random4Chars() + random4Chars() + random4Chars()); | ||
} | ||
/** | ||
* Recursive HTML visitor to render real HTML contents from parsed files | ||
*/ | ||
class Visitor extends compiler.RecursiveVisitor { | ||
s4() { | ||
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); | ||
}; | ||
/** | ||
* Uniq ID generator for automatically inserted template references | ||
*/ | ||
uniqId() { | ||
return (this.s4() + this.s4() + this.s4() + this.s4() + this.s4() + this.s4() + this.s4() + this.s4()); | ||
} | ||
/** | ||
* Set if templates should be generated | ||
@@ -101,3 +101,3 @@ */ | ||
if(!!this.templateGeneration) { | ||
this.templateIds.push(this.uniqId()); | ||
this.templateIds.push(uniqId()); | ||
this.templates.push('<ng-template #automaticallyGeneratedTemplate' + this.templateIds[this.templateElementCount] + '>' + element + '</ng-template>'); | ||
@@ -171,18 +171,13 @@ } | ||
let config = getLoaderConfig(this); | ||
let binding = config.hasOwnProperty('localeBinding') && 'string' === typeof config.localeBinding && config.localeBinding.length ? config.localeBinding : 'i18nLocaleBindingProperty'; | ||
let result = content; | ||
let formats = {'xliff': 'Xliff', 'xlf': 'Xliff', 'xliff2': 'Xliff2', 'xlf2': 'Xliff2', 'xmb': 'Xmb', 'xtb': 'Xtb'}; | ||
if(!config.hasOwnProperty('enabled') || !config.enabled) { | ||
return result; | ||
return content; | ||
} | ||
if('i18nLocaleBindingProperty' === binding) { | ||
console.warn('It seems there was no "localeBinding" specified in the config, falling back to default "i18nLocaleBindingProperty".'); | ||
} | ||
if(!config.hasOwnProperty('translationFiles') || !Array.isArray(config.translationFiles) || 1 > config.translationFiles.length) { | ||
if(!config.hasOwnProperty('translationFiles') || !Array.isArray(config.translationFiles) || | ||
1 > config.translationFiles.filter(function(file) { return !!('string' === typeof file && 0 < file.length); }).length) { | ||
console.warn('It seems there were no "translationFiles" specified in the config, skipping.'); | ||
return result; | ||
return content; | ||
} | ||
@@ -193,5 +188,6 @@ | ||
return result; | ||
return content; | ||
} | ||
let result = content; | ||
let format = config.translationFormat.toLowerCase(); | ||
@@ -213,2 +209,3 @@ let serializer = new compiler[formats[format]](); | ||
let templatesGenerated = false; | ||
let identifier = uniqId(); | ||
@@ -234,3 +231,3 @@ config.translationFiles.forEach(function(file) { | ||
containers += '<ng-container *ngSwitchCase="\'' + String(locale) + '\'">' + compiler.visitAll(visitor, parsed.rootNodes).join('') + '</ng-container>'; | ||
containers += '<ng-container *ngI18nAot="\'' + identifier + '\'; locale: \'' + String(locale) + '\'">' + compiler.visitAll(visitor, parsed.rootNodes).join('') + '</ng-container>'; | ||
} | ||
@@ -257,5 +254,6 @@ else { | ||
containers += '<ng-container *ngSwitchDefault>' + compiler.visitAll(visitor, parsed.rootNodes).join('') + '</ng-container>'; | ||
result = '<ng-container [ngSwitch]="' + binding + '">' + containers + '</ng-container>' + visitor.getTemplates(true); | ||
result = | ||
containers + | ||
'<ng-container *ngI18nAot="\'' + identifier + '\'; isDefault: true">' + compiler.visitAll(visitor, parsed.rootNodes).join('') + '</ng-container>' + | ||
visitor.getTemplates(true); | ||
} | ||
@@ -262,0 +260,0 @@ else { |
@@ -11,3 +11,3 @@ { | ||
"license": "MIT", | ||
"version": "0.0.5", | ||
"version": "1.0.0", | ||
"author": { | ||
@@ -38,14 +38,49 @@ "name": "Gabriel Schuster - actra.development", | ||
}, | ||
"devDependencies": {}, | ||
"peerDependencies": {}, | ||
"devDependencies": { | ||
"mkdirp": "0.5.1", | ||
"ncp": "2.0.0", | ||
"npm-run-all": "4.0.2", | ||
"rimraf": "2.6.1", | ||
"rollup": "0.43.0", | ||
"uglify-js": "3.0.21", | ||
"yarn": "0.24.6" | ||
}, | ||
"peerDependencies": { | ||
"@angular/core": "^4.1.0" | ||
}, | ||
"files": [ | ||
"index.js", | ||
"LICENSE", | ||
"package.json", | ||
"README.md" | ||
"dist/index.js", | ||
"dist/index.umd.js", | ||
"dist/index.umd.min.js", | ||
"dist/CHANGELOG.md", | ||
"dist/LICENSE", | ||
"dist/package.json", | ||
"dist/README.md" | ||
], | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
"test": "echo \"Error: no test specified\" && exit 1", | ||
"build": "yarn run run-s -- build:clean build:copy", | ||
"build:clean": "yarn run rimraf -- ./dist && yarn run mkdirp -- ./dist", | ||
"build:copy": "yarn run run-s -- build:copy-1 build:copy-2 build:copy-3 build:copy-4 build:copy-5", | ||
"build:copy-1": "yarn run ncp -- ./package.json ./dist/package.json", | ||
"build:copy-2": "yarn run ncp -- ./CHANGELOG.md ./dist/CHANGELOG.md", | ||
"build:copy-3": "yarn run ncp -- ./index.js ./dist/index.js", | ||
"build:copy-4": "yarn run ncp -- ./LICENSE ./dist/LICENSE", | ||
"build:copy-5": "yarn run ncp -- ./README.md ./dist/README.md", | ||
"publishToNpmjs": "yarn run build && npm publish ./dist --access public --registry https://registry.npmjs.com/", | ||
"mkdirp": "./node_modules/.bin/mkdirp", | ||
"ncp": "./node_modules/.bin/ncp", | ||
"rimraf": "./node_modules/.bin/rimraf", | ||
"rollup": "./node_modules/.bin/rollup", | ||
"run-s": "./node_modules/.bin/run-s", | ||
"uglifyjs": "./node_modules/.bin/uglifyjs", | ||
"yarn": "./node_modules/.bin/yarn" | ||
} | ||
} |
144
README.md
@@ -9,6 +9,7 @@ # ng-i18n-aot-loader [Proof-Of-Concept] | ||
## What it does | ||
The loader modifies the application's HTML before it get's passed to angular's compiler by rendering it for each locale and wrapping all translations into `ng-container`s with `ng-switch`. | ||
The loader modifies the application's HTML before it get's passed to angular's compiler by rendering it for each locale and wrapping all translations into `ng-container`s. | ||
If you're using `ng-content` or `router-outlet` inside your templates those get filtered out and replaced by a template reference injected to the end of the HTML because they may only occur once per | ||
document but would occur multiple times (your number of locales + 1) after modification. | ||
All bindings and contexts stay intact and there's no need to do special magic to your code. | ||
All bindings and contexts stay intact and there's no need to do special magic to your code. | ||
To actually change the displayed locale, a service is provided (by `@actra-development-oss/ng-i18n-aot-module`) that you may include in your component(s) to call `setLocale('new_locale')` on it. | ||
@@ -21,3 +22,3 @@ | ||
So the process would be: `change html` => `extract texts` => `update translations` => `rebuild html`. | ||
Depending on your dev setup webpack may to the HTML rebuild automatically for you when the translation files change. | ||
Depending on your dev setup webpack may do the HTML rebuild automatically for you when the translation files change. | ||
If not you can trigger a HTML rebuild for individual files simply by updating the desired file, e.g. adding a new-line, and saving it for webpack to catch the change. | ||
@@ -44,30 +45,28 @@ | ||
```html | ||
<ng-container [ngSwitch]="locale"> | ||
<ng-container *ngSwitchCase="'de-DE'"> | ||
<div class="mat-app-background"> | ||
<ng-container *ngTemplateOutlet="automaticallyGeneratedTemplate1"></ng-container> | ||
<md-chip-list> | ||
<md-chip>Erster Chip</md-chip> | ||
<md-chip color="primary" title="Zweiter Chip :)">Zweiter Chip</md-chip> | ||
<md-chip color="accent">Dritter Chip</md-chip> | ||
</md-chip-list> | ||
<ng-container *ngTemplateOutlet="automaticallyGeneratedTemplate2"></ng-container> | ||
</div> | ||
</ng-container> | ||
<ng-container *ngSwitcDefault> | ||
<div class="mat-app-background"> | ||
<ng-container *ngTemplateOutlet="automaticallyGeneratedTemplate1"></ng-container> | ||
<md-chip-list> | ||
<md-chip>First chip</md-chip> | ||
<md-chip color="primary" title="Second chip :-)">Second chip</md-chip> | ||
<md-chip color="accent">Third chip</md-chip> | ||
</md-chip-list> | ||
<ng-container *ngTemplateOutlet="automaticallyGeneratedTemplate2"></ng-container> | ||
</div> | ||
</ng-container> | ||
<ng-container *ngI18nAot="'automaticallyGeneratedUniqueIdPerHtmlFile'; locale: 'de-DE'"> | ||
<div class="mat-app-background"> | ||
<ng-container *ngTemplateOutlet="automaticallyGeneratedTemplate1"></ng-container> | ||
<md-chip-list> | ||
<md-chip>Erster Chip</md-chip> | ||
<md-chip color="primary" title="Zweiter Chip :)">Zweiter Chip</md-chip> | ||
<md-chip color="accent">Dritter Chip</md-chip> | ||
</md-chip-list> | ||
<ng-container *ngTemplateOutlet="automaticallyGeneratedTemplate2"></ng-container> | ||
</div> | ||
</ng-container> | ||
<ng-container *ngI18nAot="'automaticallyGeneratedUniqueIdPerHtmlFile'; isDefault: true"> | ||
<div class="mat-app-background"> | ||
<ng-container *ngTemplateOutlet="automaticallyGeneratedTemplate1"></ng-container> | ||
<md-chip-list> | ||
<md-chip>First chip</md-chip> | ||
<md-chip color="primary" title="Second chip :-)">Second chip</md-chip> | ||
<md-chip color="accent">Third chip</md-chip> | ||
</md-chip-list> | ||
<ng-container *ngTemplateOutlet="automaticallyGeneratedTemplate2"></ng-container> | ||
</div> | ||
</ng-container> | ||
<ng-template #automaticallyGeneratedTemplate1><ng-content></ng-content></ng-template> | ||
@@ -85,3 +84,3 @@ <ng-template #automaticallyGeneratedTemplate2><router-outlet></router-outlet></ng-template> | ||
## How to use it | ||
Install the package `npm install @actra-development-oss/ng-i18n-aot-loader` and define it as a pre-loader in your webpack config: | ||
Install the packages `npm install @actra-development-oss/ng-i18n-aot-loader @actra-development-oss/ng-i18n-aot-module` and define the loader as a pre-loader in your webpack config: | ||
```js | ||
@@ -98,4 +97,3 @@ module: { | ||
enabled: true, | ||
localeBinding: 'locale', | ||
translationFiles: glob.sync('/path/to/src/locales/**/messages.*.xlf'), | ||
translationFiles: ['/path/to/src/locales/messages.de.xlf'], | ||
translationFormat: 'xliff' | ||
@@ -116,37 +114,68 @@ } | ||
``` | ||
NB: I'm using `glob.sync()` for the option `translationFiles` for convenience, any other tool would suffice as long as the result is an array of strings, you may even specify the paths by hand. | ||
In every component that has a translatable template you now need to specify the public property `locale` in order for the `ng-switch` to fire: | ||
Include the module into your applications main module: | ||
```typescript | ||
@Component({ | ||
import { NgI18nAotModule } from '@actra-development-oss/ng-i18n-aot-module'; | ||
// ... | ||
@NgModule({ | ||
// ... | ||
imports: [ | ||
NgI18nAotModule.forRoot(), | ||
// ... | ||
], | ||
// ... | ||
}) | ||
export class MyComponent { | ||
public locale: string = 'en_US'; | ||
@Injectable() | ||
export class ApplicationModule { | ||
// ... | ||
} | ||
``` | ||
To actually switch the locale, the component has to be notified of changes to the locale, e.g. by subscribing to a service, using a redux-store or whatever you like. | ||
In my test-project I used redux with it's `@select()`-syntax and subscribed my components like so: | ||
Include the module into a) every module that uses translations *OR* b) once into your shared module that is included in all your modules. | ||
Code shows option b), include in shared module: | ||
```typescript | ||
@Component({ | ||
import { NgI18nAotModule } from '@actra-development-oss/ng-i18n-aot-module'; | ||
// ... | ||
@NgModule({ | ||
// ... | ||
exports: [ | ||
NgI18nAotModule, | ||
// ... | ||
], | ||
// ... | ||
}) | ||
export class MyComponent { | ||
@select(['application', 'locale']) public locale$: Observable<string>; | ||
export class SharedModule { | ||
} | ||
``` | ||
As this now is an observable the loader-config needs to be sligthly adjusted so the `localeBinding` is recognized as async: | ||
To actually change the displayed locale use the service: | ||
```typescript | ||
// ... | ||
use: [ | ||
{ | ||
loader: '@actra-development-oss/ng-i18n-aot-loader', | ||
options: { | ||
localeBinding: 'locale$ | async' | ||
} | ||
} | ||
] | ||
// ... | ||
import { NgI18nAotService } from '@actra-development-oss/ng-i18n-aot-module'; | ||
@Component({ | ||
// ... | ||
template: ` | ||
<button (click)="setLocale('en_US')">en_US</button> <button (click)="setLocale('de_DE')">de_DE</button><br /> | ||
Current locale: {{locale}} | ||
` | ||
}) | ||
export class MyComponent { | ||
public locale: string; | ||
constructor(protected ngI18nAotService: NgI18nAotService) { | ||
this.locale = this.ngI18nAotService.getLocale(); | ||
} | ||
public setLocale(locale: string): void { | ||
this.locale = locale; | ||
this.ngI18nAotService.setLocale(this.locale); | ||
} | ||
} | ||
``` | ||
@@ -159,10 +188,3 @@ | ||
| enabled | boolean | Whether the loader should modify the HTML or not. | | ||
| localeBinding | string | Name of your component's property that holds the locale.<br />When using an observable don't forget to specify as such:<br />e.g. `locale$ \| async` | | ||
| translationFiles | string[] | Paths of all your locale files to render. | | ||
| translationFormat | string | Format of the translation files as used by angular:<br />xlf / xliff, xlf2 / xliff2, xmb, xtb | | ||
## Known caveats | ||
As `ng-switch` on the used `ng-containers` removes the dom entirely, angular may (think to) detect expression changes after the view has been checked. | ||
This simply is a timing problem, I didn't find a solid workaround for this until now. | ||
As those console messages are disabled for production builds by angular just forget about them. |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
21150
5
1
184
6
7