├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── extensions.json ├── LICENSE.md ├── README.md ├── angular.json ├── apps ├── .gitkeep ├── demo-e2e │ ├── cypress.json │ ├── src │ │ ├── fixtures │ │ │ └── example.json │ │ ├── integration │ │ │ └── app.spec.ts │ │ ├── plugins │ │ │ └── index.js │ │ └── support │ │ │ ├── app.po.ts │ │ │ ├── commands.ts │ │ │ └── index.ts │ ├── tsconfig.e2e.json │ ├── tsconfig.json │ └── tslint.json ├── demo │ ├── browserslist │ ├── jest.config.js │ ├── src │ │ ├── app │ │ │ ├── app-routing.module.ts │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ └── lazy-loaded │ │ │ │ ├── lazy-loaded.component.ts │ │ │ │ └── lazy-loaded.module.ts │ │ ├── assets │ │ │ ├── .gitkeep │ │ │ └── i18n │ │ │ │ ├── fr.json │ │ │ │ └── lazy.fr.json │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── i18n-setup.ts │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ └── test-setup.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── tslint.json ├── electron │ ├── .gitignore │ ├── src │ │ ├── icons │ │ │ ├── icon.icns │ │ │ ├── icon.ico │ │ │ └── icon.png │ │ ├── index.ts │ │ └── package.json │ └── tsconfig.json ├── web-e2e │ ├── cypress.json │ ├── src │ │ ├── fixtures │ │ │ └── example.json │ │ ├── integration │ │ │ └── app.spec.ts │ │ ├── plugins │ │ │ └── index.js │ │ └── support │ │ │ ├── app.po.ts │ │ │ ├── commands.ts │ │ │ └── index.ts │ ├── tsconfig.e2e.json │ ├── tsconfig.json │ └── tslint.json └── web │ ├── browserslist │ ├── jest.config.js │ ├── src │ ├── app │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.electron.module.ts │ │ └── app.module.ts │ ├── assets │ │ ├── .gitkeep │ │ └── img │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-256x256.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── bg.jpg │ │ │ ├── bg.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── locl-full.png │ │ │ ├── locl-social.png │ │ │ ├── mstile-150x150.png │ │ │ ├── safari-pinned-tab.svg │ │ │ └── site.webmanifest │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.electron.ts │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ └── test-setup.ts │ ├── tsconfig.electron.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── tslint.json ├── commitlint.config.js ├── decorate-angular-cli.js ├── jest.config.js ├── lerna.json ├── libs ├── .gitkeep ├── app │ ├── src │ │ ├── electron │ │ │ ├── .xplatframework │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ ├── core.module.ts │ │ │ │ ├── index.ts │ │ │ │ └── services │ │ │ │ ├── electron.service.ts │ │ │ │ └── index.ts │ │ ├── test-setup.ts │ │ └── web │ │ │ ├── .xplatframework │ │ │ ├── index.ts │ │ │ ├── scss │ │ │ ├── _index.scss │ │ │ ├── _spacing.scss │ │ │ ├── _tags.scss │ │ │ ├── _variables.scss │ │ │ └── package.json │ │ │ └── src │ │ │ ├── core.module.spec.ts │ │ │ ├── core.module.ts │ │ │ ├── index.ts │ │ │ └── services │ │ │ ├── index.ts │ │ │ ├── log.service.ts │ │ │ ├── tokens.ts │ │ │ └── window.service.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── cli │ ├── README.md │ ├── ng-package.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── cmds │ │ │ ├── common │ │ │ │ ├── diagnostics.ts │ │ │ │ ├── file_utils.ts │ │ │ │ └── util.ts │ │ │ ├── convert.ts │ │ │ ├── convert │ │ │ │ ├── convert.ts │ │ │ │ ├── message_serialization │ │ │ │ │ ├── message_renderer.ts │ │ │ │ │ ├── message_serializer.ts │ │ │ │ │ └── target_message_renderer.ts │ │ │ │ ├── translation_parsers │ │ │ │ │ ├── simple_json_translation_parser.ts │ │ │ │ │ ├── translation_parse_error.ts │ │ │ │ │ ├── translation_parser.ts │ │ │ │ │ ├── translation_utils.ts │ │ │ │ │ ├── xliff1_translation_parser.ts │ │ │ │ │ ├── xliff2_translation_parser.ts │ │ │ │ │ └── xtb_translation_parser.ts │ │ │ │ └── translations.ts │ │ │ ├── extract.ts │ │ │ └── extract │ │ │ │ ├── extract.ts │ │ │ │ ├── extractor.ts │ │ │ │ ├── source_file_utils.ts │ │ │ │ ├── source_files │ │ │ │ ├── es2015_extract_plugin.ts │ │ │ │ └── es5_extract_plugin.ts │ │ │ │ └── translation_files │ │ │ │ ├── json_translation_serializer.ts │ │ │ │ ├── translation_serializer.ts │ │ │ │ ├── xliff1_translation_serializer.ts │ │ │ │ ├── xliff2_translation_serializer.ts │ │ │ │ ├── xmb_translation_serializer.ts │ │ │ │ ├── xml_file.ts │ │ │ │ └── xtb_translation_serializer.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── cli.module.ts │ │ ├── locl │ │ ├── main.ts │ │ ├── test-setup.ts │ │ └── tsconfig.json │ ├── test │ │ └── cmds │ │ │ ├── convert.spec.ts │ │ │ ├── extract.spec.ts │ │ │ └── mock.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── common │ ├── ng-package.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── common.module.spec.ts │ │ │ └── common.module.ts │ │ └── test-setup.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── core │ ├── README.md │ ├── ng-package.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── core.module.ts │ │ │ ├── global.ts │ │ │ ├── init.ts │ │ │ └── interfaces.ts │ │ └── test-setup.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── scss │ ├── _index.scss │ ├── _variables.scss │ └── package.json └── utils │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── index.ts │ ├── lib │ │ ├── angular.ts │ │ ├── objects.ts │ │ └── platform.ts │ └── test-setup.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── nx.json ├── package-lock.json ├── package.json ├── testing ├── karma.conf.js ├── test.libs.ts ├── test.xplat.ts ├── tsconfig.libs.json ├── tsconfig.libs.spec.json ├── tsconfig.xplat.json └── tsconfig.xplat.spec.json ├── tools ├── commitlint-plugin-body-content │ ├── commitlint-plugin-body-content.js │ └── package.json ├── electron │ └── postinstall.js ├── schematics │ └── .gitkeep ├── tsconfig.tools.json └── web │ └── postinstall.js ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = false 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ocombe] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | .released-packages 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | 42 | # libs 43 | libs/**/*.js 44 | libs/**/*.map 45 | libs/**/*.d.ts 46 | libs/**/*.metadata.json 47 | libs/**/*.ngfactory.ts 48 | libs/**/*.ngsummary.json 49 | 50 | # xplat 51 | xplat/**/*.js 52 | xplat/**/*.map 53 | xplat/**/*.d.ts 54 | xplat/**/*.metadata.json 55 | xplat/**/*.ngfactory.ts 56 | xplat/**/*.ngsummary.json 57 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | **/xplat/*/.xplatframework -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "angular.ng-template", 5 | "ms-vscode.vscode-typescript-tslint-plugin", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 🌐 **Locl is an internationalization (i18n) tools suite for Angular.** 4 | 5 | ### Demos 6 | You can find a complete demo in the [apps/demo folder](apps/demo) and another one simpler [on StackBlitz](https://stackblitz.com/edit/ivy-ovy4cd) (it can take a long time to load the first time because ivy support on StackBlitz is still WIP). 7 | 8 | ### Core 9 | A library with various utility functions to help you with \$localize. 10 | 11 | - [Documentation](libs/core) 12 | 13 | ### CLI 14 | Dev tools to help you with `$localize` and Angular i18n. 15 | 16 | - [Documentation](libs/cli) 17 | 18 | ### Roadmap 19 | We have a bunch of features planned on our roadmap, but you can see the features requested by the community and vote for the ones that you want the most on https://locl.hellonext.co/. 20 | 21 | ### License 22 | 23 | Locl tools and libraries are distributed under the [AGPL v3.0 license](). 24 | You can use them for free in open source projects, but if you want to use them in commercial products with close source you are required to buy a proprietary license (coming soon). 25 | -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/demo-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "pluginsFile": "./src/plugins/index", 6 | "supportFile": false, 7 | "video": true, 8 | "videosFolder": "../../dist/cypress/apps/web-demo-e2e/videos", 9 | "screenshotsFolder": "../../dist/cypress/apps/web-demo-e2e/screenshots", 10 | "chromeWebSecurity": false 11 | } 12 | -------------------------------------------------------------------------------- /apps/demo-e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /apps/demo-e2e/src/integration/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { getGreeting } from '../support/app.po'; 2 | 3 | describe('web-demo', () => { 4 | beforeEach(() => cy.visit('/')); 5 | 6 | it('should display welcome message', () => { 7 | getGreeting().contains('Welcome to web-demo!'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /apps/demo-e2e/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor'); 15 | 16 | module.exports = (on, config) => { 17 | // `on` is used to hook into various events Cypress emits 18 | // `config` is the resolved Cypress config 19 | 20 | // Preprocess Typescript 21 | on('file:preprocessor', preprocessTypescript(config)); 22 | }; 23 | -------------------------------------------------------------------------------- /apps/demo-e2e/src/support/app.po.ts: -------------------------------------------------------------------------------- 1 | export const getGreeting = () => cy.get('h1'); 2 | -------------------------------------------------------------------------------- /apps/demo-e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /apps/demo-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | -------------------------------------------------------------------------------- /apps/demo-e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc" 6 | }, 7 | "include": ["src/**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/demo-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["cypress", "node"] 5 | }, 6 | "include": ["**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /apps/demo-e2e/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": [], 4 | "linterOptions": { 5 | "exclude": ["!**/*"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/demo/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /apps/demo/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'web-demo', 3 | preset: '../../jest.config.js', 4 | coverageDirectory: '../../coverage/apps/web-demo', 5 | snapshotSerializers: [ 6 | 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 7 | 'jest-preset-angular/build/AngularSnapshotSerializer.js', 8 | 'jest-preset-angular/build/HTMLCommentSerializer.js' 9 | ] 10 | }; 11 | -------------------------------------------------------------------------------- /apps/demo/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | import { getTranslations, ParsedTranslationBundle } from '@locl/core'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: 'lazy', 9 | loadChildren: () => 10 | getTranslations('/assets/i18n/lazy.fr.json').then( 11 | (data: ParsedTranslationBundle) => { 12 | return import('./lazy-loaded/lazy-loaded.module').then( 13 | mod => mod.LazyLoadedModule 14 | ); 15 | } 16 | ) 17 | } 18 | ]; 19 | 20 | @NgModule({ 21 | declarations: [], 22 | imports: [CommonModule, RouterModule.forRoot(routes)], 23 | exports: [RouterModule] 24 | }) 25 | export class AppRoutingModule {} 26 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

{{ title }}!

2 |

3 | It works! {{ title }} 4 |

5 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Remove template code below 3 | */ 4 | :host { 5 | display: block; 6 | font-family: sans-serif; 7 | min-width: 300px; 8 | max-width: 600px; 9 | margin: 50px auto; 10 | } 11 | 12 | .gutter-left { 13 | margin-left: 9px; 14 | } 15 | 16 | .col-span-2 { 17 | grid-column: span 2; 18 | } 19 | 20 | .flex { 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | } 25 | 26 | header { 27 | background-color: #143055; 28 | color: white; 29 | padding: 5px; 30 | border-radius: 3px; 31 | } 32 | 33 | main { 34 | padding: 0 36px; 35 | } 36 | 37 | p { 38 | text-align: center; 39 | } 40 | 41 | h1 { 42 | text-align: center; 43 | margin-left: 18px; 44 | font-size: 24px; 45 | } 46 | 47 | h2 { 48 | text-align: center; 49 | font-size: 20px; 50 | margin: 40px 0 10px 0; 51 | } 52 | 53 | .resources { 54 | text-align: center; 55 | list-style: none; 56 | padding: 0; 57 | display: grid; 58 | grid-gap: 9px; 59 | grid-template-columns: 1fr 1fr; 60 | } 61 | 62 | .resource { 63 | color: #0094ba; 64 | height: 36px; 65 | background-color: rgba(0, 0, 0, 0); 66 | border: 1px solid rgba(0, 0, 0, 0.12); 67 | border-radius: 4px; 68 | padding: 3px 9px; 69 | text-decoration: none; 70 | } 71 | 72 | .resource:hover { 73 | background-color: rgba(68, 138, 255, 0.04); 74 | } 75 | 76 | pre { 77 | padding: 9px; 78 | border-radius: 4px; 79 | background-color: black; 80 | color: #eee; 81 | } 82 | 83 | details { 84 | border-radius: 4px; 85 | color: #333; 86 | background-color: rgba(0, 0, 0, 0); 87 | border: 1px solid rgba(0, 0, 0, 0.12); 88 | padding: 3px 9px; 89 | margin-bottom: 9px; 90 | } 91 | 92 | summary { 93 | cursor: pointer; 94 | outline: none; 95 | height: 36px; 96 | line-height: 36px; 97 | } 98 | 99 | .github-star-container { 100 | margin-top: 12px; 101 | line-height: 20px; 102 | } 103 | 104 | .github-star-container a { 105 | display: flex; 106 | align-items: center; 107 | text-decoration: none; 108 | color: #333; 109 | } 110 | 111 | .github-star-badge { 112 | color: #24292e; 113 | display: flex; 114 | align-items: center; 115 | font-size: 12px; 116 | padding: 3px 10px; 117 | border: 1px solid rgba(27, 31, 35, 0.2); 118 | border-radius: 3px; 119 | background-image: linear-gradient(-180deg, #fafbfc, #eff3f6 90%); 120 | margin-left: 4px; 121 | font-weight: 600; 122 | } 123 | 124 | .github-star-badge:hover { 125 | background-image: linear-gradient(-180deg, #f0f3f6, #e6ebf1 90%); 126 | border-color: rgba(27, 31, 35, 0.35); 127 | background-position: -0.5em; 128 | } 129 | .github-star-badge .material-icons { 130 | height: 16px; 131 | width: 16px; 132 | margin-right: 4px; 133 | } 134 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [AppComponent] 8 | }).compileComponents(); 9 | })); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have as title 'web-demo'`, () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.debugElement.componentInstance; 20 | expect(app.title).toEqual('web-demo'); 21 | }); 22 | 23 | it('should render title', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | fixture.detectChanges(); 26 | const compiled = fixture.debugElement.nativeElement; 27 | expect(compiled.querySelector('h1').textContent).toContain( 28 | 'Welcome to web-demo!' 29 | ); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | const name = '$localize'; 4 | const lib = 'Locl'; 5 | 6 | @Component({ 7 | selector: 'locl-root', 8 | styleUrls: ['./app.component.scss'], 9 | templateUrl: './app.component.html' 10 | }) 11 | export class AppComponent { 12 | title = $localize`Welcome to the demo of ${name} and ${lib} made for ${name}!`; 13 | 14 | constructor() { 15 | console.log($localize`:@@foo:custom id!`); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { AppRoutingModule } from './app-routing.module'; 4 | 5 | import { AppComponent } from './app.component'; 6 | 7 | @NgModule({ 8 | imports: [BrowserModule, AppRoutingModule], 9 | providers: [], 10 | declarations: [AppComponent], 11 | bootstrap: [AppComponent] 12 | }) 13 | export class AppModule {} 14 | -------------------------------------------------------------------------------- /apps/demo/src/app/lazy-loaded/lazy-loaded.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'locl-lazy-loaded', 5 | template: ` 6 |

Lazy loading works!

7 | `, 8 | styles: [] 9 | }) 10 | export class LazyLoadedComponent implements OnInit { 11 | constructor() {} 12 | 13 | ngOnInit() {} 14 | } 15 | -------------------------------------------------------------------------------- /apps/demo/src/app/lazy-loaded/lazy-loaded.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | import { LazyLoadedComponent } from './lazy-loaded.component'; 5 | 6 | const lazyRoutes: Routes = [ 7 | { 8 | path: '', 9 | component: LazyLoadedComponent 10 | } 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [CommonModule, RouterModule.forChild(lazyRoutes)], 15 | declarations: [LazyLoadedComponent] 16 | }) 17 | export class LazyLoadedModule {} 18 | -------------------------------------------------------------------------------- /apps/demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/demo/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/demo/src/assets/i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale": "fr", 3 | "translations": { 4 | "143924624516046945": "Ça fonctionne! {$INTERPOLATION}", 5 | "6586379816467235622": "Bienvenue à la démo de {$PH} et {$PH_1} fait pour {$PH_2}!", 6 | "foo": "id personnalisé" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/demo/src/assets/i18n/lazy.fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale": "fr", 3 | "translations": { 4 | "2945825304347416089": "Chargement asynchrone fonctionne !" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /apps/demo/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /apps/demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/demo/src/favicon.ico -------------------------------------------------------------------------------- /apps/demo/src/i18n-setup.ts: -------------------------------------------------------------------------------- 1 | // import { computeMsgId } from '@angular/compiler'; 2 | // import { loadTranslations } from '@angular/localize'; 3 | 4 | /*************************************************************************************************** 5 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. 6 | */ 7 | import '@angular/localize/init'; 8 | // import * as fr from './assets/i18n/fr.json'; 9 | // loadTranslations((fr as any).default.translations); 10 | // console.log('hey',computeMsgId('Welcome to {$INTERPOLATION}!', '')); 11 | -------------------------------------------------------------------------------- /apps/demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebDemo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { getTranslations, ParsedTranslationBundle } from '@locl/core'; 4 | import { AppModule } from './app/app.module'; 5 | 6 | import { environment } from './environments/environment'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | getTranslations('/assets/i18n/fr.json').then( 13 | (data: ParsedTranslationBundle) => { 14 | platformBrowserDynamic() 15 | .bootstrapModule(AppModule) 16 | .catch(err => console.error(err)); 17 | } 18 | ); 19 | -------------------------------------------------------------------------------- /apps/demo/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | -------------------------------------------------------------------------------- /apps/demo/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /apps/demo/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | import '@angular/localize/init'; 3 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [], 6 | "resolveJsonModule": true 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["**/*.ts"], 10 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest"], 5 | "resolveJsonModule": true 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/demo/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "locl", "camelCase"], 5 | "component-selector": [true, "element", "locl", "kebab-case"] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["!**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/electron/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.js.map -------------------------------------------------------------------------------- /apps/electron/src/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/electron/src/icons/icon.icns -------------------------------------------------------------------------------- /apps/electron/src/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/electron/src/icons/icon.ico -------------------------------------------------------------------------------- /apps/electron/src/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/electron/src/icons/icon.png -------------------------------------------------------------------------------- /apps/electron/src/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain, screen } from 'electron'; 2 | import * as path from 'path'; 3 | import * as url from 'url'; 4 | 5 | let serve; 6 | const args = process.argv.slice(1); 7 | serve = args.some(val => val === '--serve'); 8 | 9 | let win: Electron.BrowserWindow = null; 10 | 11 | const getFromEnv = parseInt(process.env.ELECTRON_IS_DEV, 10) === 1; 12 | const isEnvSet = 'ELECTRON_IS_DEV' in process.env; 13 | const debugMode = isEnvSet 14 | ? getFromEnv 15 | : process.defaultApp || 16 | /node_modules[\\/]electron[\\/]/.test(process.execPath); 17 | 18 | /** 19 | * Electron window settings 20 | */ 21 | const mainWindowSettings: Electron.BrowserWindowConstructorOptions = { 22 | frame: true, 23 | resizable: true, 24 | focusable: true, 25 | fullscreenable: true, 26 | kiosk: false, 27 | webPreferences: { 28 | devTools: debugMode, 29 | nodeIntegration: true 30 | } 31 | }; 32 | 33 | /** 34 | * Hooks for electron main process 35 | */ 36 | function initMainListener() { 37 | ipcMain.on('ELECTRON_BRIDGE_HOST', (event, msg) => { 38 | console.log('msg received', msg); 39 | if (msg === 'ping') { 40 | event.sender.send('ELECTRON_BRIDGE_CLIENT', 'pong'); 41 | } 42 | }); 43 | } 44 | 45 | /** 46 | * Create main window presentation 47 | */ 48 | function createWindow() { 49 | const sizes = screen.getPrimaryDisplay().workAreaSize; 50 | 51 | if (debugMode) { 52 | process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'; 53 | 54 | mainWindowSettings.width = 800; 55 | mainWindowSettings.height = 600; 56 | } else { 57 | mainWindowSettings.width = sizes.width; 58 | mainWindowSettings.height = sizes.height; 59 | mainWindowSettings.x = 0; 60 | mainWindowSettings.y = 0; 61 | } 62 | 63 | win = new BrowserWindow(mainWindowSettings); 64 | 65 | let launchPath; 66 | if (serve) { 67 | require('electron-reload')(__dirname, { 68 | electron: require(`${__dirname}/../../../node_modules/electron`) 69 | }); 70 | launchPath = 'http://localhost:4200'; 71 | win.loadURL(launchPath); 72 | } else { 73 | launchPath = url.format({ 74 | pathname: path.join(__dirname, 'index.html'), 75 | protocol: 'file:', 76 | slashes: true 77 | }); 78 | win.loadURL(launchPath); 79 | } 80 | 81 | console.log('launched electron with:', launchPath); 82 | 83 | win.on('closed', () => { 84 | // Dereference the window object, usually you would store windows 85 | // in an array if your app supports multi windows, this is the time 86 | // when you should delete the corresponding element. 87 | win = null; 88 | }); 89 | 90 | initMainListener(); 91 | 92 | if (debugMode) { 93 | // Open the DevTools. 94 | win.webContents.openDevTools(); 95 | // client.create(applicationRef); 96 | } 97 | } 98 | 99 | try { 100 | app.on('ready', createWindow); 101 | 102 | app.on('window-all-closed', () => { 103 | if (process.platform !== 'darwin') { 104 | app.quit(); 105 | } 106 | }); 107 | 108 | app.on('activate', () => { 109 | // On macOS it's common to re-create a window in the app when the 110 | // dock icon is clicked and there are no other windows open. 111 | if (win === null) { 112 | createWindow(); 113 | } 114 | }); 115 | } catch (err) { 116 | } 117 | -------------------------------------------------------------------------------- /apps/electron/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@locl/app", 3 | "version": "0.0.1", 4 | "description": "Translation app by Locl", 5 | "main": "index.js", 6 | "author": { 7 | "name": "Olivier Combe", 8 | "email": "olivier@locl.app" 9 | }, 10 | "homepage": "https://www.locl.app/", 11 | "repository": { 12 | "url": "https://github.com/loclapp/locl" 13 | }, 14 | "license": "MIT", 15 | "build": { 16 | "appId": "com.locl.app", 17 | "productName": "App", 18 | "copyright": "Copyright © 2018-2019 locl", 19 | "asar": false, 20 | "npmRebuild": false, 21 | "directories": { 22 | "buildResources": "icons", 23 | "output": "../electronapp-packages" 24 | }, 25 | "mac": { 26 | "category": "public.app-category.developer-tools", 27 | "icon": "icon.png" 28 | }, 29 | "win": { 30 | "target": "nsis", 31 | "icon": "icon.ico" 32 | }, 33 | "linux": { 34 | "icon": "icon.png", 35 | "target": [ 36 | "AppImage", 37 | "deb", 38 | "tar.gz" 39 | ], 40 | "synopsis": "App", 41 | "category": "Development" 42 | }, 43 | "nsis": { 44 | "createDesktopShortcut": "always", 45 | "installerIcon": "icon.ico", 46 | "artifactName": "App-Setup-${version}.${ext}" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/electron/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "esModuleInterop": true, 9 | "target": "es5", 10 | "typeRoots": [ 11 | "../../node_modules/@types" 12 | ], 13 | "lib": [ 14 | "es2017", 15 | "es2016", 16 | "es2015", 17 | "dom" 18 | ] 19 | }, 20 | "include": [ 21 | "src/index.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /apps/web-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "pluginsFile": "./src/plugins/index", 6 | "supportFile": false, 7 | "video": true, 8 | "videosFolder": "../../dist/cypress/apps/web-e2e/videos", 9 | "screenshotsFolder": "../../dist/cypress/apps/web-e2e/screenshots", 10 | "chromeWebSecurity": false 11 | } 12 | -------------------------------------------------------------------------------- /apps/web-e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /apps/web-e2e/src/integration/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { getGreeting } from '../support/app.po'; 2 | 3 | describe('web', () => { 4 | beforeEach(() => cy.visit('/')); 5 | 6 | it('should display welcome message', () => { 7 | getGreeting().contains('Welcome to web!'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /apps/web-e2e/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor'); 15 | 16 | module.exports = (on, config) => { 17 | // `on` is used to hook into various events Cypress emits 18 | // `config` is the resolved Cypress config 19 | 20 | // Preprocess Typescript 21 | on('file:preprocessor', preprocessTypescript(config)); 22 | }; 23 | -------------------------------------------------------------------------------- /apps/web-e2e/src/support/app.po.ts: -------------------------------------------------------------------------------- 1 | export const getGreeting = () => cy.get('h1'); 2 | -------------------------------------------------------------------------------- /apps/web-e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /apps/web-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; -------------------------------------------------------------------------------- /apps/web-e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc" 6 | }, 7 | "include": [ 8 | "src/**/*.ts" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/web-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "cypress", 6 | "node" 7 | ] 8 | }, 9 | "include": [ 10 | "**/*.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /apps/web-e2e/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": [], 4 | "linterOptions": { 5 | "exclude": ["!**/*"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /apps/web/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'web', 3 | preset: '../../jest.config.js', 4 | coverageDirectory: '../../coverage/apps/web', 5 | snapshotSerializers: [ 6 | 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 7 | 'jest-preset-angular/build/AngularSnapshotSerializer.js', 8 | 'jest-preset-angular/build/HTMLCommentSerializer.js' 9 | ] 10 | }; 11 | -------------------------------------------------------------------------------- /apps/web/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/web/src/app/app.component.scss -------------------------------------------------------------------------------- /apps/web/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [AppComponent] 8 | }).compileComponents(); 9 | })); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have as title 'web'`, () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.debugElement.componentInstance; 20 | expect(app.title).toEqual('web'); 21 | }); 22 | 23 | it('should render title in a h1 tag', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | fixture.detectChanges(); 26 | const compiled = fixture.debugElement.nativeElement; 27 | expect(compiled.querySelector('h1').textContent).toContain( 28 | 'Welcome to web!' 29 | ); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /apps/web/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'locl-root', 5 | styleUrls: ['./app.component.scss'], 6 | template: ` 7 |
8 |

Welcome to {{ title }}!

9 | 10 | 11 |
12 | ` 13 | }) 14 | export class AppComponent { 15 | title = 'web'; 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/src/app/app.electron.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { LoclElectronCoreModule } from '@locl/app-electron'; 3 | import { AppComponent } from './app.component'; 4 | import { AppModule } from './app.module'; 5 | 6 | @NgModule({ 7 | imports: [AppModule, LoclElectronCoreModule], 8 | bootstrap: [AppComponent] 9 | }) 10 | export class AppElectronModule { 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { CoreModule } from '@locl/app-web'; 4 | 5 | import { AppComponent } from './app.component'; 6 | 7 | @NgModule({ 8 | declarations: [AppComponent], 9 | imports: [CoreModule, BrowserModule], 10 | providers: [], 11 | bootstrap: [AppComponent] 12 | }) 13 | export class AppModule { 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/web/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/web/src/assets/img/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/web/src/assets/img/android-chrome-192x192.png -------------------------------------------------------------------------------- /apps/web/src/assets/img/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/web/src/assets/img/android-chrome-256x256.png -------------------------------------------------------------------------------- /apps/web/src/assets/img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/web/src/assets/img/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/web/src/assets/img/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/web/src/assets/img/bg.jpg -------------------------------------------------------------------------------- /apps/web/src/assets/img/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/web/src/assets/img/bg.png -------------------------------------------------------------------------------- /apps/web/src/assets/img/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /apps/web/src/assets/img/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/web/src/assets/img/favicon-16x16.png -------------------------------------------------------------------------------- /apps/web/src/assets/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/web/src/assets/img/favicon-32x32.png -------------------------------------------------------------------------------- /apps/web/src/assets/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/web/src/assets/img/favicon.ico -------------------------------------------------------------------------------- /apps/web/src/assets/img/locl-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/web/src/assets/img/locl-full.png -------------------------------------------------------------------------------- /apps/web/src/assets/img/locl-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/web/src/assets/img/locl-social.png -------------------------------------------------------------------------------- /apps/web/src/assets/img/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/web/src/assets/img/mstile-150x150.png -------------------------------------------------------------------------------- /apps/web/src/assets/img/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/src/assets/img/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/assets/img/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/assets/img/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /apps/web/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /apps/web/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/apps/web/src/favicon.ico -------------------------------------------------------------------------------- /apps/web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Locl - I18n tools suite for Angular 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /apps/web/src/main.electron.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppElectronModule } from './app/app.electron.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppElectronModule) 13 | .catch(err => console.log(err)); 14 | -------------------------------------------------------------------------------- /apps/web/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /apps/web/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ -------------------------------------------------------------------------------- /apps/web/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '~@locl/scss'; 3 | @import "~bulma/sass/utilities/_all"; 4 | @import "~bulma/sass/base/_all"; 5 | 6 | /*** ELEMENTS ***/ 7 | //@import "~bulma/sass/elements/box.sass"; 8 | @import "~bulma/sass/elements/button.sass"; 9 | //@import "~bulma/sass/elements/container.sass"; 10 | //@import "~bulma/sass/elements/content.sass"; 11 | //@import "~bulma/sass/elements/icon.sass"; 12 | //@import "~bulma/sass/elements/image.sass"; 13 | //@import "~bulma/sass/elements/notification.sass"; 14 | //@import "~bulma/sass/elements/progress.sass"; 15 | //@import "~bulma/sass/elements/table.sass"; 16 | //@import "~bulma/sass/elements/tag.sass"; 17 | //@import "~bulma/sass/elements/title.sass"; 18 | //@import "~bulma/sass/elements/other.sass"; 19 | 20 | /*** FORMS ***/ 21 | @import "~bulma/sass/form/shared.sass"; 22 | @import "~bulma/sass/form/input-textarea.sass"; 23 | //@import "~bulma/sass/form/checkbox-radio.sass"; 24 | //@import "~bulma/sass/form/select.sass"; 25 | //@import "~bulma/sass/form/file.sass"; 26 | //@import "~bulma/sass/form/tools.sass"; 27 | 28 | /*** COMPONENTS ***/ 29 | //@import "~bulma/sass/components/breadcrumb.sass"; 30 | //@import "~bulma/sass/components/card.sass"; 31 | //@import "~bulma/sass/components/dropdown.sass"; 32 | //@import "~bulma/sass/components/level.sass"; 33 | //@import "~bulma/sass/components/list.sass"; 34 | //@import "~bulma/sass/components/media.sass"; 35 | //@import "~bulma/sass/components/menu.sass"; 36 | //@import "~bulma/sass/components/message.sass"; 37 | //@import "~bulma/sass/components/modal.sass"; 38 | //@import "~bulma/sass/components/navbar.sass"; 39 | //@import "~bulma/sass/components/pagination.sass"; 40 | //@import "~bulma/sass/components/panel.sass"; 41 | //@import "~bulma/sass/components/tabs.sass"; 42 | 43 | /*** GRID ***/ 44 | @import "~bulma/sass/grid/columns.sass"; 45 | @import "~bulma/sass/grid/tiles.sass"; 46 | 47 | /*** LAYOUT ***/ 48 | //@import "~bulma/sass/layout/hero.sass"; 49 | //@import "~bulma/sass/layout/section.sass"; 50 | //@import "~bulma/sass/layout/footer.sass"; -------------------------------------------------------------------------------- /apps/web/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; -------------------------------------------------------------------------------- /apps/web/tsconfig.electron.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/main.electron.ts", 5 | "src/polyfills.ts" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "include": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "exclude": [ 12 | "src/test-setup.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": [ 7 | "jest", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "src/test-setup.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "locl", "camelCase"], 5 | "component-selector": [true, "element", "locl", "kebab-case"] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["!**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '@commitlint/config-conventional', 4 | '@commitlint/config-lerna-scopes', 5 | ], 6 | plugins: ['commitlint-plugin-body-content'], 7 | rules: { 8 | 'body-content': [2, 'always', ['affects:', ['fix', 'feat', 'perf']]], 9 | 'footer-max-line-length': [0], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /decorate-angular-cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file decorates the Angular CLI with the Nx CLI to enable features such as computation caching 3 | * and faster execution of tasks. 4 | * 5 | * It does this by: 6 | * 7 | * - Patching the Angular CLI to warn you in case you accidentally use the undecorated ng command. 8 | * - Symlinking the ng to nx command, so all commands run through the Nx CLI 9 | * - Updating the package.json postinstall script to give you control over this script 10 | * 11 | * The Nx CLI decorates the Angular CLI, so the Nx CLI is fully compatible with it. 12 | * Every command you run should work the same when using the Nx CLI, except faster. 13 | * 14 | * Because of symlinking you can still type `ng build/test/lint` in the terminal. The ng command, in this case, 15 | * will point to nx, which will perform optimizations before invoking ng. So the Angular CLI is always invoked. 16 | * The Nx CLI simply does some optimizations before invoking the Angular CLI. 17 | * 18 | * To opt out of this patch: 19 | * - Replace occurrences of nx with ng in your package.json 20 | * - Remove the script from your postinstall script in your package.json 21 | * - Delete and reinstall your node_modules 22 | */ 23 | 24 | const fs = require('fs'); 25 | const os = require('os'); 26 | const cp = require('child_process'); 27 | const isWindows = os.platform() === 'win32'; 28 | const { output } = require('@nrwl/workspace'); 29 | 30 | /** 31 | * Paths to files being patched 32 | */ 33 | const angularCLIInitPath = 'node_modules/@angular/cli/lib/cli/index.js'; 34 | 35 | /** 36 | * Patch index.js to warn you if you invoke the undecorated Angular CLI. 37 | */ 38 | function patchAngularCLI(initPath) { 39 | const angularCLIInit = fs.readFileSync(initPath, 'utf-8').toString(); 40 | 41 | if (!angularCLIInit.includes('NX_CLI_SET')) { 42 | fs.writeFileSync( 43 | initPath, 44 | ` 45 | if (!process.env['NX_CLI_SET']) { 46 | const { output } = require('@nrwl/workspace'); 47 | output.warn({ title: 'The Angular CLI was invoked instead of the Nx CLI. Use "npx ng [command]" or "nx [command]" instead.' }); 48 | } 49 | ${angularCLIInit} 50 | ` 51 | ); 52 | } 53 | } 54 | 55 | /** 56 | * Symlink of ng to nx, so you can keep using `ng build/test/lint` and still 57 | * invoke the Nx CLI and get the benefits of computation caching. 58 | */ 59 | function symlinkNgCLItoNxCLI() { 60 | try { 61 | const ngPath = './node_modules/.bin/ng'; 62 | const nxPath = './node_modules/.bin/nx'; 63 | if (isWindows) { 64 | /** 65 | * This is the most reliable way to create symlink-like behavior on Windows. 66 | * Such that it works in all shells and works with npx. 67 | */ 68 | ['', '.cmd', '.ps1'].forEach((ext) => { 69 | fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext)); 70 | }); 71 | } else { 72 | // If unix-based, symlink 73 | cp.execSync(`ln -sf ./nx ${ngPath}`); 74 | } 75 | } catch (e) { 76 | output.error({ 77 | title: 78 | 'Unable to create a symlink from the Angular CLI to the Nx CLI:' + 79 | e.message, 80 | }); 81 | throw e; 82 | } 83 | } 84 | 85 | try { 86 | symlinkNgCLItoNxCLI(); 87 | patchAngularCLI(angularCLIInitPath); 88 | output.log({ 89 | title: 'Angular CLI has been decorated to enable computation caching.', 90 | }); 91 | } catch (e) { 92 | output.error({ 93 | title: 'Decoration of the Angular CLI did not complete successfully', 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'], 3 | transform: { 4 | '^.+\\.(ts|js|html)$': 'ts-jest' 5 | }, 6 | resolver: '@nrwl/jest/plugins/resolver', 7 | moduleFileExtensions: ['ts', 'js', 'html'], 8 | coverageReporters: ['html'] 9 | }; 10 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "dist/libs/*", 4 | "dist/apps/*" 5 | ], 6 | "version": "independent" 7 | } 8 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/libs/.gitkeep -------------------------------------------------------------------------------- /libs/app/src/electron/.xplatframework: -------------------------------------------------------------------------------- 1 | angular -------------------------------------------------------------------------------- /libs/app/src/electron/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /libs/app/src/electron/src/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, Optional, SkipSelf } from '@angular/core'; 2 | 3 | import { throwIfAlreadyLoaded } from '@locl/utils'; 4 | import { ELECTRON_PROVIDERS, ElectronService } from './services'; 5 | 6 | @NgModule({ 7 | providers: [...ELECTRON_PROVIDERS] 8 | }) 9 | export class LoclElectronCoreModule { 10 | constructor( 11 | @Optional() 12 | @SkipSelf() 13 | parentModule: LoclElectronCoreModule, 14 | private _electronService: ElectronService 15 | ) { 16 | throwIfAlreadyLoaded(parentModule, 'LoclElectronCoreModule'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /libs/app/src/electron/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './services'; 2 | export { LoclElectronCoreModule } from './core.module'; 3 | -------------------------------------------------------------------------------- /libs/app/src/electron/src/services/electron.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { LogService, WindowService } from '@locl/app-web'; 3 | import { isElectron } from '@locl/utils'; 4 | import * as childProcess from 'child_process'; 5 | import { ipcRenderer, IpcRendererEvent } from 'electron'; 6 | 7 | @Injectable() 8 | export class ElectronService { 9 | private _ipc: typeof ipcRenderer; 10 | private _childProcess: typeof childProcess; 11 | 12 | constructor(private _log: LogService, private _win: WindowService) { 13 | // Conditional imports 14 | if (isElectron()) { 15 | this._ipc = this._win.require('electron').ipcRenderer; 16 | this._childProcess = this._win.require('child_process'); 17 | this._log.debug('ElectronService ready.'); 18 | } 19 | } 20 | 21 | public on(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): void { 22 | if (!this._ipc) { 23 | return; 24 | } 25 | 26 | this._ipc.on(channel, listener); 27 | } 28 | 29 | public send(channel: string, ...args): void { 30 | if (!this._ipc) { 31 | return; 32 | } 33 | 34 | this._ipc.send(channel, ...args); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /libs/app/src/electron/src/services/index.ts: -------------------------------------------------------------------------------- 1 | import { ElectronService } from './electron.service'; 2 | 3 | export const ELECTRON_PROVIDERS: any[] = [ElectronService]; 4 | 5 | export * from './electron.service'; 6 | -------------------------------------------------------------------------------- /libs/app/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; -------------------------------------------------------------------------------- /libs/app/src/web/.xplatframework: -------------------------------------------------------------------------------- 1 | angular -------------------------------------------------------------------------------- /libs/app/src/web/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /libs/app/src/web/scss/_index.scss: -------------------------------------------------------------------------------- 1 | // shared across all platforms and apps 2 | @import '../../../../../node_modules/@locl/scss/index'; 3 | /** 4 | * The following are web specific (used with any web app targets) 5 | */ 6 | // web specific variables 7 | @import 'variables'; 8 | // web styles (create/import other scss files or define as needed) 9 | @import 'spacing'; 10 | @import 'tags'; 11 | -------------------------------------------------------------------------------- /libs/app/src/web/scss/_spacing.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Convenient spacing classes 3 | */ 4 | $spacer: 5 !default; 5 | $spacer-x: $spacer !default; 6 | $spacer-y: $spacer !default; 7 | $spacer-alt: 4 !default; 8 | $spacer-x-alt: $spacer-alt !default; 9 | $spacer-y-alt: $spacer-alt !default; 10 | $spacers: ( 11 | 0: ( 12 | x: 0, 13 | y: 0 14 | ), 15 | 2: ( 16 | x: 2, 17 | y: 2 18 | ), 19 | 4: ( 20 | x: $spacer-x-alt, 21 | y: $spacer-y-alt 22 | ), 23 | 5: ( 24 | x: $spacer-x, 25 | y: $spacer-y 26 | ), 27 | 8: ( 28 | x: $spacer-x-alt * 2, 29 | y: $spacer-y-alt * 2 30 | ), 31 | 10: ( 32 | x: ( 33 | $spacer-x * 2 34 | ), 35 | y: ( 36 | $spacer-y * 2 37 | ) 38 | ), 39 | 12: ( 40 | x: $spacer-x-alt * 3, 41 | y: $spacer-y-alt * 3 42 | ), 43 | 15: ( 44 | x: ( 45 | $spacer-x * 3 46 | ), 47 | y: ( 48 | $spacer-y * 3 49 | ) 50 | ), 51 | 16: ( 52 | x: $spacer-x-alt * 4, 53 | y: $spacer-y-alt * 4 54 | ), 55 | 20: ( 56 | x: ( 57 | $spacer-x * 4 58 | ), 59 | y: ( 60 | $spacer-y * 4 61 | ) 62 | ), 63 | 24: ( 64 | x: $spacer-x-alt * 6, 65 | y: $spacer-y-alt * 6 66 | ), 67 | 25: ( 68 | x: ( 69 | $spacer-x * 5 70 | ), 71 | y: ( 72 | $spacer-y * 5 73 | ) 74 | ), 75 | 28: ( 76 | x: $spacer-x-alt * 7, 77 | y: $spacer-y-alt * 7 78 | ), 79 | 30: ( 80 | x: ( 81 | $spacer-x * 6 82 | ), 83 | y: ( 84 | $spacer-y * 6 85 | ) 86 | ) 87 | ) !default; 88 | 89 | /** 90 | * Margin and Padding 91 | * The following creates this pattern: 92 | * .m-0{margin:0}.m-t-0{margin-top:0}.m-r-0{margin-right:0}.m-b-0{margin-bottom:0}.m-l-0{margin-left:0}.m-x-0{margin-right:0;margin-left:0}.m-y-0{margin-top:0;margin-bottom:0} 93 | * Same for Padding (using the 'p' abbreviation) 94 | * From 0, 2, 5, 10, 15, 20, 25, 30 95 | **/ 96 | // sass-lint:disable-all 97 | @each $prop, $abbrev in (margin: m, padding: p) { 98 | // sass-lint:enable-all 99 | @each $size, $lengths in $spacers { 100 | $length-x: map-get($lengths, x); 101 | $length-y: map-get($lengths, y); 102 | 103 | // sass-lint:disable-all 104 | .#{$abbrev}-#{$size} { 105 | #{$prop}: #{$length-y}px; 106 | } // a = All sides (can just use one length) 107 | // sass-lint:enable-all 108 | .#{$abbrev}-t-#{$size} { 109 | #{$prop}-top: #{$length-y}px; 110 | } 111 | .#{$abbrev}-r-#{$size} { 112 | #{$prop}-right: #{$length-x}px; 113 | } 114 | .#{$abbrev}-b-#{$size} { 115 | #{$prop}-bottom: #{$length-y}px; 116 | } 117 | .#{$abbrev}-l-#{$size} { 118 | #{$prop}-left: #{$length-x}px; 119 | } 120 | 121 | // Axes 122 | .#{$abbrev}-x-#{$size} { 123 | #{$prop}-right: #{$length-x}px; 124 | #{$prop}-left: #{$length-x}px; 125 | } 126 | 127 | .#{$abbrev}-y-#{$size} { 128 | #{$prop}-top: #{$length-y}px; 129 | #{$prop}-bottom: #{$length-y}px; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /libs/app/src/web/scss/_tags.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Element tag overrides 3 | */ 4 | body { 5 | padding: 0; 6 | margin: 0; 7 | } 8 | -------------------------------------------------------------------------------- /libs/app/src/web/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // web specific variables here... 2 | -------------------------------------------------------------------------------- /libs/app/src/web/scss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@locl/web-scss", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /libs/app/src/web/src/core.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { CoreModule } from './core.module'; 2 | 3 | describe('LoclCoreModule', () => { 4 | it('should work', () => { 5 | expect(new CoreModule(null)).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /libs/app/src/web/src/core.module.ts: -------------------------------------------------------------------------------- 1 | import { APP_BASE_HREF } from '@angular/common'; 2 | import { HttpClientModule } from '@angular/common/http'; 3 | import { NgModule, Optional, SkipSelf } from '@angular/core'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { throwIfAlreadyLoaded } from '@locl/utils'; 6 | // libs 7 | import { 8 | CORE_PROVIDERS, 9 | PlatformLanguageToken, 10 | PlatformWindowToken, 11 | WindowPlatformService 12 | } from './services'; 13 | 14 | // bring in custom web services here... 15 | 16 | // factories 17 | export function winFactory() { 18 | return window; 19 | } 20 | 21 | export function platformLangFactory() { 22 | const browserLang = window.navigator.language || 'en'; // fallback English 23 | // browser language has 2 codes, ex: 'en-US' 24 | return browserLang.split('-')[0]; 25 | } 26 | 27 | export const BASE_PROVIDERS: any[] = [ 28 | ...CORE_PROVIDERS, 29 | { 30 | provide: APP_BASE_HREF, 31 | useValue: '/' 32 | } 33 | ]; 34 | 35 | @NgModule({ 36 | imports: [ 37 | BrowserModule, 38 | HttpClientModule 39 | ], 40 | providers: [ 41 | ...BASE_PROVIDERS, { 42 | provide: PlatformLanguageToken, 43 | useFactory: platformLangFactory 44 | }, { 45 | provide: PlatformWindowToken, 46 | useFactory: winFactory 47 | }, { 48 | provide: WindowPlatformService, 49 | useFactory: winFactory 50 | } 51 | ] 52 | }) 53 | export class CoreModule { 54 | constructor( 55 | @Optional() 56 | @SkipSelf() 57 | parentModule: CoreModule 58 | ) { 59 | throwIfAlreadyLoaded(parentModule, 'CoreModule'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /libs/app/src/web/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './services'; 2 | export { CoreModule } from './core.module'; 3 | -------------------------------------------------------------------------------- /libs/app/src/web/src/services/index.ts: -------------------------------------------------------------------------------- 1 | import { LogService } from './log.service'; 2 | import { WindowService } from './window.service'; 3 | 4 | export const CORE_PROVIDERS: any[] = [LogService, WindowService]; 5 | 6 | export * from './log.service'; 7 | export * from './window.service'; 8 | export * from './tokens'; 9 | -------------------------------------------------------------------------------- /libs/app/src/web/src/services/log.service.ts: -------------------------------------------------------------------------------- 1 | // angular 2 | import { Injectable } from '@angular/core'; 3 | 4 | export interface IDebug { 5 | LEVEL_1: boolean; 6 | LEVEL_2: boolean; 7 | LEVEL_3: boolean; 8 | LEVEL_4: boolean; 9 | LEVEL_5: boolean; 10 | 11 | [key: string]: boolean; 12 | } 13 | 14 | @Injectable() 15 | export class LogService { 16 | public static DEBUG: IDebug = { 17 | LEVEL_1: false, // .warn only 18 | LEVEL_2: false, // .error only 19 | LEVEL_3: false, // .log + all the above 20 | LEVEL_4: false, // .log + all the above + info 21 | LEVEL_5: false // just info (excluding all else) 22 | }; 23 | 24 | // info (extra messages like analytics) 25 | // use LEVEL_5 to see only these 26 | public info(...msg: Array) { 27 | if (LogService.DEBUG.LEVEL_5 || LogService.DEBUG.LEVEL_4) { 28 | // extra messages 29 | console.info(msg); 30 | } 31 | } 32 | 33 | // debug (standard output) 34 | public debug(...msg: Array) { 35 | if (LogService.DEBUG.LEVEL_4 || LogService.DEBUG.LEVEL_3) { 36 | // console.debug does not work on {N} apps... use `log` 37 | console.log(msg); 38 | } 39 | } 40 | 41 | // error 42 | public error(...err: Array) { 43 | if ( 44 | LogService.DEBUG.LEVEL_4 || 45 | LogService.DEBUG.LEVEL_3 || 46 | LogService.DEBUG.LEVEL_2 47 | ) { 48 | console.error(err); 49 | } 50 | } 51 | 52 | // warn 53 | public warn(...warn: Array) { 54 | if ( 55 | LogService.DEBUG.LEVEL_4 || 56 | LogService.DEBUG.LEVEL_3 || 57 | LogService.DEBUG.LEVEL_1 58 | ) { 59 | console.warn(warn); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /libs/app/src/web/src/services/tokens.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | /** 4 | * Various InjectionTokens shared across all platforms 5 | * Always suffix with 'Token' for clarity and consistency 6 | */ 7 | 8 | export const PlatformLanguageToken = new InjectionToken( 9 | 'PlatformLanguage' 10 | ); 11 | 12 | export const PlatformWindowToken = new InjectionToken( 13 | 'PlatformWindow' 14 | ); 15 | -------------------------------------------------------------------------------- /libs/app/src/web/src/services/window.service.ts: -------------------------------------------------------------------------------- 1 | // angular 2 | import { Injectable } from '@angular/core'; 3 | // app 4 | import { isObject } from '@locl/utils'; 5 | 6 | @Injectable() 7 | export class WindowPlatformService { 8 | public navigator: any = {}; 9 | public location: any = {}; 10 | public localStorage: any; 11 | public process: any; 12 | public require: any; 13 | 14 | public alert(msg: any) {} 15 | 16 | public confirm(msg: any) {} 17 | 18 | public setTimeout(handler: (...args: any[]) => void, timeout?: number) { 19 | return 0; 20 | } 21 | 22 | public clearTimeout(timeoutId: number) {} 23 | 24 | public setInterval( 25 | handler: (...args: any[]) => void, 26 | ms?: number, 27 | ...args: any[] 28 | ) { 29 | return 0; 30 | } 31 | 32 | public clearInterval(intervalId: number) {} 33 | 34 | // ...You can expand support for more window methods as you need them here... 35 | } 36 | 37 | @Injectable() 38 | export class WindowService { 39 | constructor(private _platformWindow: WindowPlatformService) { 40 | console.log('windows service'); 41 | } 42 | 43 | public get navigator() { 44 | return this._platformWindow.navigator; 45 | } 46 | 47 | public get location() { 48 | return this._platformWindow.location; 49 | } 50 | 51 | public get process() { 52 | return this._platformWindow.process; 53 | } 54 | 55 | public get require() { 56 | return this._platformWindow.require; 57 | } 58 | 59 | public alert(msg: any): Promise { 60 | return new Promise((resolve, reject) => { 61 | const result: any = this._platformWindow.alert(msg); 62 | if (isObject(result) && result.then) { 63 | // console.log('WindowService -- using result.then promise'); 64 | result.then(resolve, reject); 65 | } else { 66 | resolve(); 67 | } 68 | }); 69 | } 70 | 71 | public confirm( 72 | msg: any, 73 | action?: Function /* used for fancyalerts on mobile*/ 74 | ): Promise { 75 | return new Promise((resolve, reject) => { 76 | const result: any = (this._platformWindow).confirm(msg, undefined); 77 | if (isObject(result) && result.then) { 78 | result.then(resolve, reject); 79 | } else if (result) { 80 | resolve(); 81 | } else { 82 | reject(); 83 | } 84 | }); 85 | } 86 | 87 | public setTimeout( 88 | handler: (...args: any[]) => void, 89 | timeout?: number 90 | ): number { 91 | return this._platformWindow.setTimeout(handler, timeout); 92 | } 93 | 94 | public clearTimeout(timeoutId: number): void { 95 | return this._platformWindow.clearTimeout(timeoutId); 96 | } 97 | 98 | public setInterval( 99 | handler: (...args: any[]) => void, 100 | ms?: number, 101 | ...args: any[] 102 | ): number { 103 | return this._platformWindow.setInterval(handler, ms, args); 104 | } 105 | 106 | public clearInterval(intervalId: number): void { 107 | return this._platformWindow.clearInterval(intervalId); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /libs/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "node", 6 | "jest" 7 | ] 8 | }, 9 | "include": [ 10 | "**/*.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /libs/app/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": [ 10 | "dom", 11 | "es2018" 12 | ] 13 | }, 14 | "angularCompilerOptions": { 15 | "annotateForClosureCompiler": true, 16 | "skipTemplateCodegen": true, 17 | "strictMetadataEmit": true, 18 | "fullTemplateTypeCheck": true, 19 | "strictInjectionParameters": true, 20 | "enableResourceInlining": true 21 | }, 22 | "exclude": [ 23 | "src/test.ts", 24 | "**/*.spec.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /libs/app/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": [ 7 | "jest", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "src/test-setup.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/app/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "locl", "camelCase"], 5 | "component-selector": [true, "element", "locl", "kebab-case"] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["!**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/cli/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | # Locl CLI 4 | 5 | Dev tools to help you with `$localize` and Angular i18n. 6 | 7 | ## Installation 8 | 9 | Install the cli tools with npm: 10 | 11 | ```sh 12 | npm install @locl/cli --save-dev 13 | ``` 14 | 15 | ## Usage 16 | 17 | You can get a full list of commands with `npx locl --help`. 18 | 19 | ### Extraction 20 | 21 | You can use `locl extract` to extract translations from your ivy application: 22 | 23 | ``` 24 | npx locl extract -s="dist/apps/demo/*.js" -f=json -o="src/assets/i18n/en.json" 25 | ``` 26 | 27 | The extraction tool will find any call to `$localize` within your bundle files (in code and in templates), 28 | but there is a limitation inherent to the way Angular files are generated by the compiler: 29 | **you need to build your application with the AOT mode to generate template translations using `$localize`.** 30 | Always use `--prod` or `--aot` when you build your application. Also make sure to not build for a specific language. 31 | 32 | #### Options: 33 | 34 | - `--source` (`-s`): A glob pattern indicating what files to search for translations, e.g. `./dist/**/*.js`. This can be absolute or relative to the current working directory. 35 | - `--format` (`-f`): the format of the translation file to generate. Either `xlf`, `xlf2`, `xmb` or `json`. 36 | - `--outputPath` (`-o`): A path to where the translation file will be written. This can be absolute or relative to the current working directory. If the given path is a file, it will aggregate the translations of all the source files into one translation file. Otherwise it will generate one translation file per file parsed. 37 | - `--locale` (`-l`): The locale for the extracted file, "en" by default. 38 | 39 | ### Conversion 40 | 41 | You can use `locl convert` to convert translations from one format to another: 42 | 43 | ``` 44 | npx locl convert -s="fr.xlf" -f=json -o="src/assets/i18n/fr.json" 45 | ``` 46 | 47 | The conversion tool will read all translation files from the source glob and generate a file of the specified format at the specified output path. 48 | 49 | Since the source glob can be used to take multiple files as input, it is possible to use this tool to aggregate multiple translation files into one: 50 | 51 | ``` 52 | npx locl convert -s="src/assets/i18n/*.xlf" -f=json -o="src/assets/i18n/fr.json" 53 | ``` 54 | 55 | It is recommended to use this tool to convert your files to json if you want to lazy load the translations at bootstrap, 56 | since it is the only format that is supported by the existing loaders, and it is the most optimized one in terms of size. 57 | 58 | #### Options: 59 | 60 | - `--source` (`-s`): A glob pattern indicating what files to convert, e.g. `./assets/**/*.xlf`. This can be absolute or relative to the current working directory. Only translation files are supported (json, xtb & xlf but not xmb). 61 | - `--format` (`-f`): The format of the translation files to generate. Either `xlf`, `xlf2`, `xtb` or `json`. 62 | - `--outputPath` (`-o`): A path to where the translation file will be written. This can be absolute or relative to the current working directory. 63 | -------------------------------------------------------------------------------- /libs/cli/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/cli", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | }, 7 | "whitelistedNonPeerDependencies": [ 8 | "@babel/core", 9 | "chalk", 10 | "find-up", 11 | "glob", 12 | "yargs" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /libs/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@locl/cli", 3 | "version": "1.0.0", 4 | "license": "AGPL-3.0-or-later", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/loclapp/locl/" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/loclapp/locl/issues" 11 | }, 12 | "homepage": "https://www.locl.app/", 13 | "bin": { 14 | "locl": "./src/locl" 15 | }, 16 | "scripts": { 17 | "build": "ng build cli && npm run build:apiAndLib && npm run build:copy", 18 | "build:apiAndLib": "tsc -p src/tsconfig.json", 19 | "build:copy": "cpx src/locl ../../dist/libs/cli/src", 20 | "pretest": "npm run build", 21 | "test": "jasmine ../../dist/libs/cli/**/*spec.js" 22 | }, 23 | "dependencies": { 24 | "@babel/core": "^7.8.6", 25 | "chalk": "^4.1.0", 26 | "find-up": "^4.1.0", 27 | "glob": "^7.1.2", 28 | "yargs": "^13.1.0" 29 | }, 30 | "devDependencies": { 31 | "@types/babel__core": "^7.1.6", 32 | "@types/yargs": "^13.0.3", 33 | "@types/glob": "^7.1.1" 34 | }, 35 | "peerDependencies": { 36 | "@angular/localize": "^10.0.0", 37 | "@angular/compiler": "^10.0.0", 38 | "@angular/core": "^10.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/common/diagnostics.ts: -------------------------------------------------------------------------------- 1 | import * as chalk from 'chalk'; 2 | 3 | export declare type DiagnosticHandlingStrategy = 'error' | 'warning' | 'ignore'; 4 | 5 | /** 6 | * This class is used to collect and then report warnings and errors that occur during the execution 7 | * of the tools. 8 | */ 9 | export class Diagnostics { 10 | readonly messages = []; 11 | 12 | get hasErrors() { 13 | return this.messages.some((m) => m.type === 'error'); 14 | } 15 | 16 | add(type: DiagnosticHandlingStrategy, message: string) { 17 | if (type !== 'ignore') { 18 | this.messages.push({ type, message }); 19 | } 20 | } 21 | 22 | merge(other: Diagnostics) { 23 | this.messages.push(...other.messages); 24 | } 25 | 26 | log(message: string) { 27 | this.messages.push({ type: '', message }); 28 | } 29 | 30 | warn(message: string) { 31 | this.messages.push({ type: 'warning', message }); 32 | } 33 | 34 | error(message: string) { 35 | this.messages.push({ type: 'error', message }); 36 | } 37 | 38 | formatDiagnostics(message: string): string { 39 | const errors = this.messages!.filter((d) => d.type === 'error').map( 40 | (d) => ' - ' + d.message 41 | ); 42 | const warnings = this.messages!.filter((d) => d.type === 'warning').map( 43 | (d) => ' - ' + d.message 44 | ); 45 | if (errors.length) { 46 | message += '\nERRORS:\n' + errors.join('\n'); 47 | } 48 | if (warnings.length) { 49 | message += '\nWARNINGS:\n' + warnings.join('\n'); 50 | } 51 | return message; 52 | } 53 | 54 | logMessages() { 55 | while (this.messages.length) { 56 | const m = this.messages.shift(); 57 | switch (m.type) { 58 | case 'warning': 59 | console.warn(chalk.yellow(`Warning: ${m.message}`)); 60 | break; 61 | case 'error': 62 | console.error(chalk.red(`Error: ${m.message}`)); 63 | break; 64 | default: 65 | console.log(chalk.blue(`${m.message}`)); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/common/file_utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import * as fs from 'fs'; 9 | import * as path from 'path'; 10 | 11 | export class FileUtils { 12 | static readFile(absolutePath: string): string { 13 | return fs.readFileSync(absolutePath, 'utf8'); 14 | } 15 | 16 | static readFileBuffer(absolutePath: string): Buffer { 17 | return fs.readFileSync(absolutePath); 18 | } 19 | 20 | static writeFile(absolutePath: string, contents: string | Buffer) { 21 | FileUtils.ensureDir(path.dirname(absolutePath)); 22 | fs.writeFileSync(absolutePath, contents); 23 | } 24 | 25 | static ensureDir(absolutePath: string): void { 26 | const parents: string[] = []; 27 | while (!FileUtils.isRoot(absolutePath) && !fs.existsSync(absolutePath)) { 28 | parents.push(absolutePath); 29 | absolutePath = path.dirname(absolutePath); 30 | } 31 | while (parents.length) { 32 | fs.mkdirSync(parents.pop()!); 33 | } 34 | } 35 | 36 | static remove(p: string): void { 37 | const stat = fs.statSync(p); 38 | if (stat.isFile()) { 39 | fs.unlinkSync(p); 40 | } else if (stat.isDirectory()) { 41 | fs.readdirSync(p).forEach(child => { 42 | const absChild = path.resolve(p, child); 43 | FileUtils.remove(absChild); 44 | }); 45 | fs.rmdirSync(p); 46 | } 47 | } 48 | 49 | static isRoot(absolutePath: string): boolean { 50 | return path.dirname(absolutePath) === absolutePath; 51 | } 52 | 53 | static dedup(files: string[], pattern: RegExp, replaceValue = ''): string[] { 54 | const filesSet = files.map(file => file.replace(pattern, replaceValue)); 55 | const dedup = []; 56 | const indexes = []; 57 | filesSet.forEach((file: string, index: number) => { 58 | if (dedup.indexOf(file) === -1) { 59 | dedup.push(file); 60 | indexes.push(index); 61 | } 62 | }); 63 | return indexes.map(index => files[index]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/convert.ts: -------------------------------------------------------------------------------- 1 | import { convertFiles, TranslationFormat } from './convert/convert'; 2 | import { resolve } from 'path'; 3 | import { Diagnostics } from './common/diagnostics'; 4 | 5 | export const command = 'convert'; 6 | export const describe = 'Convert translation files from one format to another'; 7 | export const builder = { 8 | s: { 9 | alias: 'source', 10 | required: true, 11 | describe: 12 | 'A glob pattern indicating what files to convert, e.g. `./assets/**/*.xlf`. This can be absolute or relative to the current working directory. Only translation files are supported (json, xtb & xlf but not xmb).', 13 | }, 14 | f: { 15 | alias: 'format', 16 | required: true, 17 | describe: 'The format of the translation files to generate.', 18 | choices: ['json', 'xlf', 'xtb', 'xlf2'], 19 | default: 'json', 20 | }, 21 | o: { 22 | alias: 'outputPath', 23 | required: true, 24 | describe: 25 | 'A path to where the converted files will be written. This can be absolute or relative to the current working directory.', 26 | }, 27 | }; 28 | 29 | export const handler = function (options) { 30 | const diagnostics = new Diagnostics(); 31 | convertFiles({ 32 | sourceGlob: resolve(options['s']), 33 | format: options['f'] as TranslationFormat, 34 | outputPath: resolve(options['o']), 35 | diagnostics, 36 | }); 37 | diagnostics.logMessages(); 38 | process.exit(diagnostics.hasErrors ? 1 : 0); 39 | }; 40 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/convert/convert.ts: -------------------------------------------------------------------------------- 1 | import { NodeJSFileSystem } from '@angular/compiler-cli/src/ngtsc/file_system'; 2 | import { TranslationLoader } from '@angular/localize/src/tools/src/translate/translation_files/translation_loader'; 3 | import { Diagnostics } from '../common/diagnostics'; 4 | import { FileUtils } from '../common/file_utils'; 5 | import { getTranslationSerializer, translationToMessage } from '../common/util'; 6 | import { SimpleJsonTranslationParser } from '../convert/translation_parsers/simple_json_translation_parser'; 7 | import { Xliff1TranslationParser } from '../convert/translation_parsers/xliff1_translation_parser'; 8 | import { Xliff2TranslationParser } from '../convert/translation_parsers/xliff2_translation_parser'; 9 | import { XtbTranslationParser } from '../convert/translation_parsers/xtb_translation_parser'; 10 | import * as glob from 'glob'; 11 | import { posix } from 'path'; 12 | 13 | export type TranslationFormat = 14 | | 'json' 15 | | 'xtb' 16 | | 'xliff1' 17 | | 'xliff2' 18 | | 'xlf' 19 | | 'xlf2'; 20 | 21 | export interface ConvertFilesOptions { 22 | sourceGlob: string; 23 | format: TranslationFormat; 24 | outputPath: string; 25 | diagnostics: Diagnostics; 26 | } 27 | 28 | export function convertFiles({ 29 | sourceGlob: source, 30 | format, 31 | outputPath: output, 32 | diagnostics, 33 | }: ConvertFilesOptions) { 34 | console.log( 35 | `Converting files from source "${source}" to format "${format}" and output "${output}"` 36 | ); 37 | const filesToProcess = glob.sync(source, { 38 | absolute: true, 39 | nodir: true, 40 | }); 41 | 42 | const fs = new NodeJSFileSystem(); 43 | 44 | const translationLoader = new TranslationLoader( 45 | fs, 46 | [ 47 | new Xliff2TranslationParser(diagnostics), 48 | new Xliff1TranslationParser(diagnostics), 49 | new XtbTranslationParser(diagnostics), 50 | new SimpleJsonTranslationParser(diagnostics), 51 | ], 52 | 'ignore', 53 | diagnostics 54 | ); 55 | 56 | // Convert all the `translationFilePaths` elements to arrays. 57 | const translationFilePathsArrays = filesToProcess.map((filePaths) => 58 | Array.isArray(filePaths) 59 | ? filePaths.map((p) => fs.resolve(p)) 60 | : [fs.resolve(filePaths)] 61 | ); 62 | 63 | const translationBundles = translationLoader.loadBundles( 64 | translationFilePathsArrays, 65 | [] 66 | ); 67 | if (translationBundles.length) { 68 | const messages = []; 69 | translationBundles.forEach((translationBundle) => { 70 | const translations = translationBundle.translations; 71 | messages.push( 72 | ...Object.keys(translations).map((id) => 73 | translationToMessage(id, translations[id]) 74 | ) 75 | ); 76 | }); 77 | 78 | const serializer = getTranslationSerializer(format); 79 | const translationFile = serializer.renderFile( 80 | messages, 81 | translationBundles[0].locale, 82 | true 83 | ); 84 | 85 | FileUtils.writeFile(posix.normalize(output), translationFile); 86 | } else { 87 | diagnostics.error(`Couldn't find any file to convert.`); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/convert/message_serialization/message_renderer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | export interface MessageRenderer { 10 | message: T; 11 | startRender(): void; 12 | endRender(): void; 13 | text(text: string): void; 14 | description(text: string): void; 15 | meaning(text: string): void; 16 | placeholder(name: string, body: string | undefined): void; 17 | startPlaceholder(name: string): void; 18 | closePlaceholder(name: string): void; 19 | startContainer(): void; 20 | closeContainer(): void; 21 | startIcu(): void; 22 | endIcu(): void; 23 | } 24 | 25 | export function stripInterpolationMarkers(interpolation: string): string { 26 | return interpolation.replace(/^\{\{/, '').replace(/}}$/, ''); 27 | } 28 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/convert/message_serialization/message_serializer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { 9 | Element, 10 | Expansion, 11 | ExpansionCase, 12 | Node, 13 | Text, 14 | visitAll 15 | } from '@angular/compiler'; 16 | import { BaseVisitor } from '@angular/localize/src/tools/src/translate/translation_files/base_visitor'; 17 | 18 | import { TranslationParseError } from '../translation_parsers/translation_parse_error'; 19 | import { 20 | getAttrOrThrow, 21 | getAttribute 22 | } from '../translation_parsers/translation_utils'; 23 | 24 | import { MessageRenderer } from './message_renderer'; 25 | 26 | interface MessageSerializerConfig { 27 | inlineElements: string[]; 28 | placeholder?: { 29 | elementName: string; 30 | nameAttribute: string; 31 | bodyAttribute?: string; 32 | }; 33 | placeholderContainer?: { 34 | elementName: string; 35 | startAttribute: string; 36 | endAttribute: string; 37 | }; 38 | } 39 | 40 | /** 41 | * This visitor will walk over a set of XML nodes, which represent an i18n message, and serialize 42 | * them into a message object of type `T`. 43 | * The type of the serialized message is controlled by the 44 | */ 45 | export class MessageSerializer extends BaseVisitor { 46 | constructor( 47 | private renderer: MessageRenderer, 48 | private config: MessageSerializerConfig 49 | ) { 50 | super(); 51 | } 52 | 53 | serialize(nodes: Node[]): T { 54 | this.renderer.startRender(); 55 | visitAll(this, nodes); 56 | this.renderer.endRender(); 57 | return this.renderer.message; 58 | } 59 | 60 | visitElement(element: Element): void { 61 | if ( 62 | this.config.placeholder && 63 | element.name === this.config.placeholder.elementName 64 | ) { 65 | const name = getAttrOrThrow( 66 | element, 67 | this.config.placeholder.nameAttribute 68 | ); 69 | const body = 70 | this.config.placeholder.bodyAttribute && 71 | getAttribute(element, this.config.placeholder.bodyAttribute); 72 | this.visitPlaceholder(name, body); 73 | } else if ( 74 | this.config.placeholderContainer && 75 | element.name === this.config.placeholderContainer.elementName 76 | ) { 77 | const start = getAttrOrThrow( 78 | element, 79 | this.config.placeholderContainer.startAttribute 80 | ); 81 | const end = getAttrOrThrow( 82 | element, 83 | this.config.placeholderContainer.endAttribute 84 | ); 85 | this.visitPlaceholderContainer(start, element.children, end); 86 | } else if (this.config.inlineElements.indexOf(element.name) !== -1) { 87 | visitAll(this, element.children); 88 | } else { 89 | throw new TranslationParseError( 90 | element.sourceSpan, 91 | `Invalid element found in message.` 92 | ); 93 | } 94 | } 95 | 96 | visitText(text: Text): void { 97 | this.renderer.text(text.value); 98 | } 99 | 100 | visitExpansion(expansion: Expansion): void { 101 | this.renderer.startIcu(); 102 | this.renderer.text(`${expansion.switchValue}, ${expansion.type},`); 103 | visitAll(this, expansion.cases); 104 | this.renderer.endIcu(); 105 | } 106 | 107 | visitExpansionCase(expansionCase: ExpansionCase): void { 108 | this.renderer.text(` ${expansionCase.value} {`); 109 | this.renderer.startContainer(); 110 | visitAll(this, expansionCase.expression); 111 | this.renderer.closeContainer(); 112 | this.renderer.text(`}`); 113 | } 114 | 115 | visitContainedNodes(nodes: Node[]): void { 116 | const length = nodes.length; 117 | let index = 0; 118 | while (index < length) { 119 | if (!this.isPlaceholderContainer(nodes[index])) { 120 | const startOfContainedNodes = index; 121 | while (index < length - 1) { 122 | index++; 123 | if (this.isPlaceholderContainer(nodes[index])) { 124 | break; 125 | } 126 | } 127 | if (index - startOfContainedNodes > 1) { 128 | // Only create a container if there are two or more contained Nodes in a row 129 | this.renderer.startContainer(); 130 | visitAll(this, nodes.slice(startOfContainedNodes, index - 1)); 131 | this.renderer.closeContainer(); 132 | } 133 | } 134 | if (index < length) { 135 | nodes[index].visit(this, undefined); 136 | } 137 | index++; 138 | } 139 | } 140 | 141 | visitPlaceholder(name: string, body: string | undefined): void { 142 | this.renderer.placeholder(name, body); 143 | } 144 | 145 | visitPlaceholderContainer( 146 | startName: string, 147 | children: Node[], 148 | closeName: string 149 | ): void { 150 | this.renderer.startPlaceholder(startName); 151 | this.visitContainedNodes(children); 152 | this.renderer.closePlaceholder(closeName); 153 | } 154 | 155 | private isPlaceholderContainer(node: Node): boolean { 156 | return ( 157 | node instanceof Element && 158 | node.name === this.config.placeholderContainer!.elementName 159 | ); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/convert/message_serialization/target_message_renderer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { MessageRenderer } from './message_renderer'; 9 | import { ParsedTranslation } from '../translations'; 10 | 11 | /** 12 | * Create a `ParsedTranslation` from a set of `messageParts` and `placeholderNames`. 13 | * 14 | * @param messageParts The message parts to appear in the ParsedTranslation. 15 | * @param placeholderNames The names of the placeholders to intersperse between the `messageParts`. 16 | */ 17 | export function makeParsedTranslation( 18 | messageParts: string[], 19 | placeholderNames: string[] = [], 20 | description?: string, 21 | meaning?: string 22 | ): ParsedTranslation { 23 | return { 24 | messageParts: makeTemplateObject(messageParts, messageParts), 25 | placeholderNames, 26 | description, 27 | meaning, 28 | text: '' 29 | }; 30 | } 31 | 32 | /** 33 | * Create the specialized array that is passed to tagged-string tag functions. 34 | * 35 | * @param cooked The message parts with their escape codes processed. 36 | * @param raw The message parts with their escaped codes as-is. 37 | */ 38 | export function makeTemplateObject( 39 | cooked: string[], 40 | raw: string[] 41 | ): TemplateStringsArray { 42 | Object.defineProperty(cooked, 'raw', { value: raw }); 43 | return cooked as any; 44 | } 45 | 46 | /** 47 | * A message renderer that outputs `ParsedTranslation` objects. 48 | */ 49 | export class TargetMessageRenderer 50 | implements MessageRenderer { 51 | private current: MessageInfo = { 52 | messageParts: [], 53 | placeholderNames: [], 54 | text: '' 55 | }; 56 | private icuDepth = 0; 57 | 58 | get message(): ParsedTranslation { 59 | const { 60 | messageParts, 61 | placeholderNames, 62 | description, 63 | meaning 64 | } = this.current; 65 | return makeParsedTranslation( 66 | messageParts, 67 | placeholderNames, 68 | description, 69 | meaning 70 | ); 71 | } 72 | startRender(): void {} 73 | endRender(): void { 74 | this.storeMessagePart(); 75 | } 76 | text(text: string): void { 77 | this.current.text += text; 78 | } 79 | description(description: string): void { 80 | this.current.description = description; 81 | } 82 | meaning(meaning: string): void { 83 | this.current.text = meaning; 84 | } 85 | placeholder(name: string, body: string | undefined): void { 86 | this.renderPlaceholder(name); 87 | } 88 | startPlaceholder(name: string): void { 89 | this.renderPlaceholder(name); 90 | } 91 | closePlaceholder(name: string): void { 92 | this.renderPlaceholder(name); 93 | } 94 | startContainer(): void {} 95 | closeContainer(): void {} 96 | startIcu(): void { 97 | this.icuDepth++; 98 | this.text('{'); 99 | } 100 | endIcu(): void { 101 | this.icuDepth--; 102 | this.text('}'); 103 | } 104 | private normalizePlaceholderName(name: string) { 105 | return name.replace(/-/g, '_'); 106 | } 107 | private renderPlaceholder(name: string) { 108 | name = this.normalizePlaceholderName(name); 109 | if (this.icuDepth > 0) { 110 | this.text(`{${name}}`); 111 | } else { 112 | this.storeMessagePart(); 113 | this.current.placeholderNames.push(name); 114 | } 115 | } 116 | private storeMessagePart() { 117 | this.current.messageParts.push(this.current.text); 118 | this.current.text = ''; 119 | } 120 | } 121 | 122 | interface MessageInfo { 123 | messageParts: string[]; 124 | placeholderNames: string[]; 125 | text: string; 126 | description?: string; 127 | meaning?: string; 128 | } 129 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/convert/translation_parsers/simple_json_translation_parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { 9 | ɵMessageId, 10 | ɵParsedTranslation, 11 | ɵparseTranslation, 12 | } from '@angular/localize'; 13 | import { Diagnostics } from '../../common/diagnostics'; 14 | import { extname } from 'path'; 15 | import { 16 | ParseAnalysis, 17 | ParsedTranslationBundle, 18 | TranslationParser, 19 | } from './translation_parser'; 20 | 21 | /** 22 | * A translation parser that can parse JSON that has the form: 23 | * 24 | * ``` 25 | * { 26 | * "locale": "...", 27 | * "translations": { 28 | * "message-id": "Target message string", 29 | * ... 30 | * } 31 | * } 32 | * ``` 33 | */ 34 | export class SimpleJsonTranslationParser implements TranslationParser { 35 | constructor(private diagnostics: Diagnostics) {} 36 | 37 | canParse(filePath: string, contents: string): Object | false { 38 | const result = this.analyze(filePath, contents); 39 | return result.canParse && result.hint; 40 | } 41 | 42 | analyze(filePath: string, contents: string): ParseAnalysis { 43 | const diagnostics = new Diagnostics(); 44 | if (extname(filePath) !== '.json') { 45 | diagnostics.warn('File does not have .json extension.'); 46 | return { canParse: false, diagnostics }; 47 | } 48 | try { 49 | const json = JSON.parse(contents); 50 | if (json.locale === undefined) { 51 | diagnostics.warn('Required "locale" property missing.'); 52 | return { canParse: false, diagnostics }; 53 | } 54 | if (typeof json.locale !== 'string') { 55 | diagnostics.warn('The "locale" property is not a string.'); 56 | return { canParse: false, diagnostics }; 57 | } 58 | if (json.translations === undefined) { 59 | diagnostics.warn('Required "translations" property missing.'); 60 | return { canParse: false, diagnostics }; 61 | } 62 | if (typeof json.translations !== 'object') { 63 | diagnostics.warn('The "translations" is not an object.'); 64 | return { canParse: false, diagnostics }; 65 | } 66 | return { canParse: true, diagnostics, hint: json }; 67 | } catch (e) { 68 | diagnostics.warn('File is not valid JSON.'); 69 | return { canParse: false, diagnostics }; 70 | } 71 | } 72 | 73 | parse(_filePath: string, contents: string): ParsedTranslationBundle { 74 | const { locale: parsedLocale, translations } = JSON.parse(contents); 75 | const parsedTranslations: Record<ɵMessageId, ɵParsedTranslation> = {}; 76 | for (const messageId in translations) { 77 | const targetMessage = translations[messageId]; 78 | parsedTranslations[messageId] = ɵparseTranslation(targetMessage); 79 | } 80 | return { 81 | locale: parsedLocale, 82 | translations: parsedTranslations, 83 | diagnostics: this.diagnostics, 84 | }; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/convert/translation_parsers/translation_parse_error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { ParseErrorLevel, ParseSourceSpan } from '@angular/compiler'; 9 | 10 | /** 11 | * This error is thrown when there is a problem parsing a translation file. 12 | */ 13 | export class TranslationParseError extends Error { 14 | constructor( 15 | public span: ParseSourceSpan, 16 | public msg: string, 17 | public level: ParseErrorLevel = ParseErrorLevel.ERROR 18 | ) { 19 | super(msg); 20 | } 21 | 22 | contextualMessage(): string { 23 | const ctx = this.span.start.getContext(100, 3); 24 | return ctx 25 | ? `${this.msg} ("${ctx.before}[${ParseErrorLevel[this.level]} ->]${ 26 | ctx.after 27 | }")` 28 | : this.msg; 29 | } 30 | 31 | toString(): string { 32 | const details = this.span.details ? `, ${this.span.details}` : ''; 33 | return `${this.contextualMessage()}: ${this.span.start}${details}`; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/convert/translation_parsers/translation_parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { Diagnostics } from '../../common/diagnostics'; 9 | import { ParsedTranslation } from '../translations'; 10 | import { ɵMessageId } from '@angular/localize'; 11 | 12 | /** 13 | * An object that holds translations that have been parsed from a translation file. 14 | */ 15 | export interface ParsedTranslationBundle { 16 | locale: string | undefined; 17 | sources?: Record<ɵMessageId, ParsedTranslation>; 18 | translations: Record<ɵMessageId, ParsedTranslation>; 19 | diagnostics: Diagnostics; 20 | } 21 | 22 | /** 23 | * Indicates that a parser can parse a given file, with a hint that can be used to speed up actual 24 | * parsing. 25 | */ 26 | export interface CanParseAnalysis { 27 | canParse: true; 28 | diagnostics: Diagnostics; 29 | hint: Hint; 30 | } 31 | 32 | /** 33 | * Indicates that a parser cannot parse a given file with diagnostics as why this is. 34 | * */ 35 | export interface CannotParseAnalysis { 36 | canParse: false; 37 | diagnostics: Diagnostics; 38 | } 39 | 40 | /** 41 | * Information about whether a `TranslationParser` can parse a given file. 42 | */ 43 | export type ParseAnalysis = CanParseAnalysis | CannotParseAnalysis; 44 | 45 | /** 46 | * Implement this interface to provide a class that can parse the contents of a translation file. 47 | */ 48 | export interface TranslationParser { 49 | /** 50 | * Parses the given file, extracting the target locale and translations. 51 | * 52 | * @param filePath The absolute path to the translation file. 53 | * @param contents The contents of the translation file. 54 | */ 55 | parse(filePath: string, contents: string): ParsedTranslationBundle; 56 | 57 | /** 58 | * Returns true if this parser can parse the given file. 59 | * 60 | * @param filePath The absolute path to the translation file. 61 | * @param contents The contents of the translation file. 62 | * @param hint A value that can be used by the parser to speed up parsing of the file. This will 63 | * have been provided as the return result from calling `canParse()`. 64 | */ 65 | canParse(filePath: string, contents: string, hint: Hint): Hint | false; 66 | 67 | /** 68 | * Analyze the file to see if this parser can parse the given file. 69 | * 70 | * @param filePath The absolute path to the translation file. 71 | * @param contents The contents of the translation file. 72 | * @returns Information indicating whether the file can be parsed by this parser. 73 | */ 74 | analyze(filePath: string, contents: string): ParseAnalysis; 75 | } 76 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/convert/translation_parsers/translation_utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { 9 | Element, 10 | LexerRange, 11 | Node, 12 | ParseError, 13 | ParseErrorLevel, 14 | ParseSourceSpan, 15 | XmlParser, 16 | } from '@angular/compiler'; 17 | import { ParseAnalysis } from './translation_parser'; 18 | import { Diagnostics } from '../../common/diagnostics'; 19 | import { TranslationParseError } from './translation_parse_error'; 20 | 21 | export function getAttrOrThrow(element: Element, attrName: string): string { 22 | const attrValue = getAttribute(element, attrName); 23 | if (attrValue === undefined) { 24 | throw new TranslationParseError( 25 | element.sourceSpan, 26 | `Missing required "${attrName}" attribute:` 27 | ); 28 | } 29 | return attrValue; 30 | } 31 | 32 | export function getAttribute( 33 | element: Element, 34 | attrName: string 35 | ): string | undefined { 36 | const attr = element.attrs.find((a) => a.name === attrName); 37 | return attr !== undefined ? attr.value : undefined; 38 | } 39 | 40 | export function parseInnerRange(element: Element): Node[] { 41 | const xmlParser = new XmlParser(); 42 | const xml = xmlParser.parse( 43 | element.sourceSpan.start.file.content, 44 | element.sourceSpan.start.file.url, 45 | { tokenizeExpansionForms: true, range: getInnerRange(element) } 46 | ); 47 | if (xml.errors.length) { 48 | throw xml.errors 49 | .map((e) => new TranslationParseError(e.span, e.msg).toString()) 50 | .join('\n'); 51 | } 52 | return xml.rootNodes; 53 | } 54 | 55 | function getInnerRange(element: Element): LexerRange { 56 | const start = element.startSourceSpan!.end; 57 | const end = element.endSourceSpan!.start; 58 | return { 59 | startPos: start.offset, 60 | startLine: start.line, 61 | startCol: start.col, 62 | endPos: end.offset, 63 | }; 64 | } 65 | 66 | /** 67 | * Create a predicate, which can be used by things like `Array.filter()`, that will match a named 68 | * XML Element from a collection of XML Nodes. 69 | * 70 | * @param name The expected name of the element to match. 71 | */ 72 | export function isNamedElement(name: string): (node: Node) => node is Element { 73 | function predicate(node: Node): node is Element { 74 | return node instanceof Element && node.name === name; 75 | } 76 | return predicate; 77 | } 78 | 79 | /** 80 | * This "hint" object is used to pass information from `canParse()` to `parse()` for 81 | * `TranslationParser`s that expect XML contents. 82 | * 83 | * This saves the `parse()` method from having to re-parse the XML. 84 | */ 85 | export interface XmlTranslationParserHint { 86 | element: Element; 87 | errors: ParseError[]; 88 | } 89 | 90 | /** 91 | * Can this XML be parsed for translations, given the expected `rootNodeName` and expected root node 92 | * `attributes` that should appear in the file. 93 | * 94 | * @param filePath The path to the file being checked. 95 | * @param contents The contents of the file being checked. 96 | * @param rootNodeName The expected name of an XML root node that should exist. 97 | * @param attributes The attributes (and their values) that should appear on the root node. 98 | * @returns The `XmlTranslationParserHint` object for use by `TranslationParser.parse()` if the XML 99 | * document has the expected format. 100 | */ 101 | export function canParseXml( 102 | filePath: string, 103 | contents: string, 104 | rootNodeName: string, 105 | attributes: Record 106 | ): ParseAnalysis { 107 | const diagnostics = new Diagnostics(); 108 | const xmlParser = new XmlParser(); 109 | const xml = xmlParser.parse(contents, filePath); 110 | 111 | if ( 112 | xml.rootNodes.length === 0 || 113 | xml.errors.some((error) => error.level === ParseErrorLevel.ERROR) 114 | ) { 115 | xml.errors.forEach((e) => addParseError(diagnostics, e)); 116 | return { canParse: false, diagnostics }; 117 | } 118 | 119 | const rootElements = xml.rootNodes.filter(isNamedElement(rootNodeName)); 120 | const rootElement = rootElements[0]; 121 | if (rootElement === undefined) { 122 | diagnostics.warn( 123 | `The XML file does not contain a <${rootNodeName}> root node.` 124 | ); 125 | return { canParse: false, diagnostics }; 126 | } 127 | 128 | for (const attrKey of Object.keys(attributes)) { 129 | const attr = rootElement.attrs.find((at) => at.name === attrKey); 130 | if (attr === undefined || attr.value !== attributes[attrKey]) { 131 | addParseDiagnostic( 132 | diagnostics, 133 | rootElement.sourceSpan, 134 | `The <${rootNodeName}> node does not have the required attribute: ${attrKey}="${attributes[attrKey]}".`, 135 | ParseErrorLevel.WARNING 136 | ); 137 | return { canParse: false, diagnostics }; 138 | } 139 | } 140 | 141 | if (rootElements.length > 1) { 142 | xml.errors.push( 143 | new ParseError( 144 | xml.rootNodes[1].sourceSpan, 145 | 'Unexpected root node. XLIFF 1.2 files should only have a single root node.', 146 | ParseErrorLevel.WARNING 147 | ) 148 | ); 149 | } 150 | 151 | return { 152 | canParse: true, 153 | diagnostics, 154 | hint: { element: rootElement, errors: xml.errors }, 155 | }; 156 | } 157 | 158 | /** 159 | * Add an XML parser related message to the given `diagnostics` object. 160 | */ 161 | export function addParseDiagnostic( 162 | diagnostics: Diagnostics, 163 | sourceSpan: ParseSourceSpan, 164 | message: string, 165 | level: ParseErrorLevel 166 | ): void { 167 | addParseError(diagnostics, new ParseError(sourceSpan, message, level)); 168 | } 169 | 170 | /** 171 | * Copy the formatted error message from the given `parseError` object into the given `diagnostics` 172 | * object. 173 | */ 174 | export function addParseError( 175 | diagnostics: Diagnostics, 176 | parseError: ParseError 177 | ): void { 178 | if (parseError.level === ParseErrorLevel.ERROR) { 179 | diagnostics.error(parseError.toString()); 180 | } else { 181 | diagnostics.warn(parseError.toString()); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/convert/translation_parsers/xliff1_translation_parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { Element, Node, XmlParser, visitAll, Text } from '@angular/compiler'; 9 | import { ɵMessageId } from '@angular/localize'; 10 | import { Diagnostics } from '../../common/diagnostics'; 11 | import { BaseVisitor } from '@angular/localize/src/tools/src/translate/translation_files/base_visitor'; 12 | 13 | import { MessageSerializer } from '../message_serialization/message_serializer'; 14 | import { TargetMessageRenderer } from '../message_serialization/target_message_renderer'; 15 | import { TranslationParseError } from './translation_parse_error'; 16 | import { 17 | ParseAnalysis, 18 | ParsedTranslationBundle, 19 | TranslationParser, 20 | } from './translation_parser'; 21 | import { 22 | getAttrOrThrow, 23 | getAttribute, 24 | parseInnerRange, 25 | canParseXml, 26 | XmlTranslationParserHint, 27 | } from './translation_utils'; 28 | import { ParsedTranslation } from '../translations'; 29 | 30 | /** 31 | * A translation parser that can load XLIFF 1.2 files. 32 | * 33 | * http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html 34 | * http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html 35 | * 36 | */ 37 | export class Xliff1TranslationParser 38 | implements TranslationParser { 39 | constructor(private diagnostics: Diagnostics) {} 40 | 41 | canParse( 42 | filePath: string, 43 | contents: string 44 | ): XmlTranslationParserHint | false { 45 | const result = this.analyze(filePath, contents); 46 | return result.canParse && result.hint; 47 | } 48 | 49 | analyze( 50 | filePath: string, 51 | contents: string 52 | ): ParseAnalysis { 53 | return canParseXml(filePath, contents, 'xliff', { version: '1.2' }); 54 | } 55 | 56 | parse(filePath: string, contents: string): ParsedTranslationBundle { 57 | const xmlParser = new XmlParser(); 58 | const xml = xmlParser.parse(contents, filePath); 59 | const bundle = XliffFileElementVisitor.extractBundle( 60 | xml.rootNodes, 61 | this.diagnostics 62 | ); 63 | if (bundle === undefined) { 64 | throw new Error(`Unable to parse "${filePath}" as XLIFF 1.2 format.`); 65 | } 66 | return bundle; 67 | } 68 | } 69 | 70 | class XliffFileElementVisitor extends BaseVisitor { 71 | private bundle: ParsedTranslationBundle | undefined; 72 | 73 | constructor(private diagnostics: Diagnostics) { 74 | super(); 75 | } 76 | 77 | static extractBundle( 78 | xliff: Node[], 79 | diagnostics: Diagnostics 80 | ): ParsedTranslationBundle | undefined { 81 | const visitor = new this(diagnostics); 82 | visitAll(visitor, xliff); 83 | return visitor.bundle; 84 | } 85 | 86 | visitElement(element: Element): any { 87 | if (element.name === 'file') { 88 | this.bundle = { 89 | locale: getAttribute(element, 'target-language'), 90 | translations: XliffTranslationVisitor.extractTranslations(element), 91 | diagnostics: this.diagnostics, 92 | }; 93 | } else { 94 | return visitAll(this, element.children); 95 | } 96 | } 97 | } 98 | 99 | class XliffTranslationVisitor extends BaseVisitor { 100 | private translations: Record<ɵMessageId, ParsedTranslation> = {}; 101 | 102 | static extractTranslations(file: Element): Record { 103 | const visitor = new this(); 104 | visitAll(visitor, file.children); 105 | return visitor.translations; 106 | } 107 | 108 | visitElement(element: Element): any { 109 | if (element.name === 'trans-unit') { 110 | const id = getAttrOrThrow(element, 'id'); 111 | if (this.translations[id] !== undefined) { 112 | throw new TranslationParseError( 113 | element.sourceSpan, 114 | `Duplicated translations for message "${id}"` 115 | ); 116 | } 117 | 118 | let meaning, description; 119 | element.children.forEach((el) => { 120 | if (isTargetElement(el)) { 121 | this.translations[id] = serializeTargetMessage(el); 122 | } else if (isSourceElement(el) && !this.translations[id]) { 123 | this.translations[id] = serializeTargetMessage(el); 124 | } else if (isNoteElement(el)) { 125 | const from = el.attrs.find((attr) => attr.name === 'from'); 126 | if (el.children.length) { 127 | const value = (el.children[0] as Text).value; 128 | if (from.value === 'description') { 129 | description = value; 130 | } else if (from.value === 'meaning') { 131 | meaning = value; 132 | } 133 | } 134 | } 135 | }); 136 | 137 | if (this.translations[id] === undefined) { 138 | throw new TranslationParseError( 139 | element.sourceSpan, 140 | 'Missing required element' 141 | ); 142 | } else { 143 | if (description) { 144 | this.translations[id].description = description; 145 | } 146 | if (meaning) { 147 | this.translations[id].meaning = meaning; 148 | } 149 | } 150 | } else { 151 | return visitAll(this, element.children); 152 | } 153 | } 154 | } 155 | 156 | function serializeTargetMessage(source: Element): ParsedTranslation { 157 | const serializer = new MessageSerializer(new TargetMessageRenderer(), { 158 | inlineElements: ['g', 'bx', 'ex', 'bpt', 'ept', 'ph', 'it', 'mrk'], 159 | placeholder: { elementName: 'x', nameAttribute: 'id' }, 160 | }); 161 | return serializer.serialize(parseInnerRange(source)); 162 | } 163 | 164 | function isSourceElement(node: Node): node is Element { 165 | return node instanceof Element && node.name === 'source'; 166 | } 167 | 168 | function isTargetElement(node: Node): node is Element { 169 | return node instanceof Element && node.name === 'target'; 170 | } 171 | 172 | function isNoteElement(node: Node): node is Element { 173 | return node instanceof Element && node.name === 'note'; 174 | } 175 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/convert/translation_parsers/xliff2_translation_parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { Element, Node, XmlParser, visitAll } from '@angular/compiler'; 9 | import { ɵMessageId, ɵParsedTranslation } from '@angular/localize'; 10 | import { Diagnostics } from '../../common/diagnostics'; 11 | 12 | import { MessageSerializer } from '../message_serialization/message_serializer'; 13 | import { TargetMessageRenderer } from '../message_serialization/target_message_renderer'; 14 | 15 | import { TranslationParseError } from './translation_parse_error'; 16 | import { 17 | ParseAnalysis, 18 | ParsedTranslationBundle, 19 | TranslationParser, 20 | } from './translation_parser'; 21 | import { 22 | getAttrOrThrow, 23 | getAttribute, 24 | parseInnerRange, 25 | XmlTranslationParserHint, 26 | canParseXml, 27 | } from './translation_utils'; 28 | import { BaseVisitor } from '@angular/localize/src/tools/src/translate/translation_files/base_visitor'; 29 | 30 | /** 31 | * A translation parser that can load translations from XLIFF 2 files. 32 | * 33 | * http://docs.oasis-open.org/xliff/xliff-core/v2.0/os/xliff-core-v2.0-os.html 34 | * 35 | */ 36 | export class Xliff2TranslationParser 37 | implements TranslationParser { 38 | constructor(private diagnostics: Diagnostics) {} 39 | 40 | canParse( 41 | filePath: string, 42 | contents: string 43 | ): XmlTranslationParserHint | false { 44 | const result = this.analyze(filePath, contents); 45 | return result.canParse && result.hint; 46 | } 47 | 48 | analyze( 49 | filePath: string, 50 | contents: string 51 | ): ParseAnalysis { 52 | return canParseXml(filePath, contents, 'xliff', { version: '2.0' }); 53 | } 54 | 55 | parse(filePath: string, contents: string): ParsedTranslationBundle { 56 | const xmlParser = new XmlParser(); 57 | const xml = xmlParser.parse(contents, filePath); 58 | const bundle = Xliff2TranslationBundleVisitor.extractBundle( 59 | xml.rootNodes, 60 | this.diagnostics 61 | ); 62 | if (bundle === undefined) { 63 | throw new Error(`Unable to parse "${filePath}" as XLIFF 2.0 format.`); 64 | } 65 | return bundle; 66 | } 67 | } 68 | 69 | interface BundleVisitorContext { 70 | parsedLocale?: string; 71 | } 72 | 73 | class Xliff2TranslationBundleVisitor extends BaseVisitor { 74 | private bundle: ParsedTranslationBundle | undefined; 75 | 76 | constructor(private diagnostics: Diagnostics) { 77 | super(); 78 | } 79 | 80 | static extractBundle( 81 | xliff: Node[], 82 | diagnostics: Diagnostics 83 | ): ParsedTranslationBundle | undefined { 84 | const visitor = new this(diagnostics); 85 | visitAll(visitor, xliff, {}); 86 | return visitor.bundle; 87 | } 88 | 89 | visitElement(element: Element, { parsedLocale }: BundleVisitorContext): any { 90 | if (element.name === 'xliff') { 91 | parsedLocale = getAttribute(element, 'trgLang'); 92 | return visitAll(this, element.children, { parsedLocale }); 93 | } else if (element.name === 'file') { 94 | this.bundle = { 95 | locale: parsedLocale, 96 | translations: Xliff2TranslationVisitor.extractTranslations(element), 97 | diagnostics: this.diagnostics, 98 | }; 99 | } else { 100 | return visitAll(this, element.children, { parsedLocale }); 101 | } 102 | } 103 | } 104 | 105 | class Xliff2TranslationVisitor extends BaseVisitor { 106 | private translations: Record<ɵMessageId, ɵParsedTranslation> = {}; 107 | 108 | static extractTranslations( 109 | file: Element 110 | ): Record { 111 | const visitor = new this(); 112 | visitAll(visitor, file.children); 113 | return visitor.translations; 114 | } 115 | 116 | visitElement(element: Element, context: any): any { 117 | if (element.name === 'unit') { 118 | const externalId = getAttrOrThrow(element, 'id'); 119 | if (this.translations[externalId] !== undefined) { 120 | throw new TranslationParseError( 121 | element.sourceSpan, 122 | `Duplicated translations for message "${externalId}"` 123 | ); 124 | } 125 | visitAll(this, element.children, { unit: externalId }); 126 | } else if (element.name === 'segment') { 127 | assertTranslationUnit(element, context); 128 | const targetMessage = element.children.find(isTargetElement); 129 | if (targetMessage === undefined) { 130 | throw new TranslationParseError( 131 | element.sourceSpan, 132 | 'Missing required element' 133 | ); 134 | } 135 | this.translations[context.unit] = serializeTargetMessage(targetMessage); 136 | } else { 137 | return visitAll(this, element.children); 138 | } 139 | } 140 | } 141 | 142 | function assertTranslationUnit(segment: Element, context: any) { 143 | if (context === undefined || context.unit === undefined) { 144 | throw new TranslationParseError( 145 | segment.sourceSpan, 146 | 'Invalid element: should be a child of a element.' 147 | ); 148 | } 149 | } 150 | 151 | function serializeTargetMessage(source: Element): ɵParsedTranslation { 152 | const serializer = new MessageSerializer(new TargetMessageRenderer(), { 153 | inlineElements: ['cp', 'sc', 'ec', 'mrk', 'sm', 'em'], 154 | placeholder: { 155 | elementName: 'ph', 156 | nameAttribute: 'equiv', 157 | bodyAttribute: 'disp', 158 | }, 159 | placeholderContainer: { 160 | elementName: 'pc', 161 | startAttribute: 'equivStart', 162 | endAttribute: 'equivEnd', 163 | }, 164 | }); 165 | return serializer.serialize(parseInnerRange(source)); 166 | } 167 | 168 | function isTargetElement(node: Node): node is Element { 169 | return node instanceof Element && node.name === 'target'; 170 | } 171 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/convert/translation_parsers/xtb_translation_parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { Element, Node, XmlParser, visitAll } from '@angular/compiler'; 9 | import { ɵParsedTranslation } from '@angular/localize'; 10 | import { extname } from 'path'; 11 | 12 | import { MessageSerializer } from '../message_serialization/message_serializer'; 13 | import { TargetMessageRenderer } from '../message_serialization/target_message_renderer'; 14 | 15 | import { TranslationParseError } from './translation_parse_error'; 16 | import { 17 | ParseAnalysis, 18 | ParsedTranslationBundle, 19 | TranslationParser, 20 | } from './translation_parser'; 21 | import { 22 | canParseXml, 23 | getAttrOrThrow, 24 | parseInnerRange, 25 | XmlTranslationParserHint, 26 | } from './translation_utils'; 27 | import { Diagnostics } from '../../common/diagnostics'; 28 | import { BaseVisitor } from '@angular/localize/src/tools/src/translate/translation_files/base_visitor'; 29 | 30 | /** 31 | * A translation parser that can load XB files. 32 | */ 33 | export class XtbTranslationParser 34 | implements TranslationParser { 35 | constructor(private diagnostics: Diagnostics) {} 36 | 37 | canParse( 38 | filePath: string, 39 | contents: string 40 | ): XmlTranslationParserHint | false { 41 | const result = this.analyze(filePath, contents); 42 | return result.canParse && result.hint; 43 | } 44 | 45 | analyze( 46 | filePath: string, 47 | contents: string 48 | ): ParseAnalysis { 49 | const extension = extname(filePath); 50 | if (extension !== '.xtb' && extension !== '.xmb') { 51 | const diagnostics = new Diagnostics(); 52 | this.diagnostics.warn('Must have xtb or xmb extension.'); 53 | return { canParse: false, diagnostics }; 54 | } 55 | return canParseXml(filePath, contents, 'translationbundle', {}); 56 | } 57 | 58 | parse(filePath: string, contents: string): ParsedTranslationBundle { 59 | const xmlParser = new XmlParser(); 60 | const xml = xmlParser.parse(contents, filePath); 61 | const bundle = XtbVisitor.extractBundle(this.diagnostics, xml.rootNodes); 62 | if (bundle === undefined) { 63 | throw new Error(`Unable to parse "${filePath}" as XTB/XMB format.`); 64 | } 65 | return bundle; 66 | } 67 | } 68 | 69 | class XtbVisitor extends BaseVisitor { 70 | static extractBundle( 71 | diagnostics: Diagnostics, 72 | messageBundles: Node[] 73 | ): ParsedTranslationBundle | undefined { 74 | const visitor = new this(diagnostics); 75 | const bundles: ParsedTranslationBundle[] = visitAll( 76 | visitor, 77 | messageBundles, 78 | undefined 79 | ); 80 | return bundles[0]; 81 | } 82 | 83 | constructor(private diagnostics: Diagnostics) { 84 | super(); 85 | } 86 | 87 | visitElement( 88 | element: Element, 89 | bundle: ParsedTranslationBundle | undefined 90 | ): any { 91 | switch (element.name) { 92 | case 'translationbundle': 93 | if (bundle) { 94 | throw new TranslationParseError( 95 | element.sourceSpan, 96 | ' elements can not be nested' 97 | ); 98 | } 99 | const langAttr = element.attrs.find((attr) => attr.name === 'lang'); 100 | bundle = { 101 | locale: langAttr && langAttr.value, 102 | translations: {}, 103 | diagnostics: this.diagnostics, 104 | }; 105 | visitAll(this, element.children, bundle); 106 | return bundle; 107 | 108 | case 'translation': 109 | if (!bundle) { 110 | throw new TranslationParseError( 111 | element.sourceSpan, 112 | ' must be inside a ' 113 | ); 114 | } 115 | const id = getAttrOrThrow(element, 'id'); 116 | if (bundle.translations.hasOwnProperty(id)) { 117 | throw new TranslationParseError( 118 | element.sourceSpan, 119 | `Duplicated translations for message "${id}"` 120 | ); 121 | } else { 122 | try { 123 | bundle.translations[id] = serializeTargetMessage(element); 124 | } catch (error) { 125 | if (typeof error === 'string') { 126 | this.diagnostics.warn( 127 | `Could not parse message with id "${id}" - perhaps it has an unrecognised ICU format?\n` + 128 | error 129 | ); 130 | } else { 131 | throw error; 132 | } 133 | } 134 | } 135 | break; 136 | 137 | default: 138 | throw new TranslationParseError(element.sourceSpan, 'Unexpected tag'); 139 | } 140 | } 141 | } 142 | 143 | function serializeTargetMessage(source: Element): ɵParsedTranslation { 144 | const serializer = new MessageSerializer(new TargetMessageRenderer(), { 145 | inlineElements: [], 146 | placeholder: { elementName: 'ph', nameAttribute: 'name' }, 147 | }); 148 | return serializer.serialize(parseInnerRange(source)); 149 | } 150 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/convert/translations.ts: -------------------------------------------------------------------------------- 1 | import { ɵMessageId, ɵParsedTranslation } from '@angular/localize'; 2 | 3 | /** 4 | * A translation message that has been processed to extract the message parts and placeholders. 5 | */ 6 | export interface ParsedTranslation extends ɵParsedTranslation { 7 | messageParts: TemplateStringsArray; 8 | placeholderNames: string[]; 9 | description?: string; 10 | meaning?: string; 11 | } 12 | 13 | /** 14 | * The internal structure used by the runtime localization to translate messages. 15 | */ 16 | export declare type ParsedTranslations = Record<ɵMessageId, ParsedTranslation>; 17 | 18 | /** 19 | * An object that holds translations that have been parsed from a translation file. 20 | */ 21 | export interface ParsedTranslationBundle { 22 | locale: string | undefined; 23 | translations: ParsedTranslations; 24 | } 25 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/extract.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostics } from './common/diagnostics'; 2 | import { TranslationFormat } from './common/util'; 3 | import { extractTranslations } from './extract/extract'; 4 | 5 | export const command = 'extract'; 6 | export const describe = 'Extract translations from your ivy application'; 7 | export const builder = { 8 | s: { 9 | alias: 'source', 10 | required: true, 11 | describe: 12 | 'A glob pattern indicating what files to search for translations, e.g. `./dist/**/*.js`. This can be absolute or relative to the current working directory.', 13 | }, 14 | f: { 15 | alias: 'format', 16 | required: true, 17 | describe: 'The format of the translation files to generate.', 18 | choices: ['json', 'xlf', 'xmb', 'xlf2'], 19 | default: 'json', 20 | }, 21 | o: { 22 | alias: 'outputPath', 23 | required: true, 24 | describe: 25 | 'A path to where the translation file will be written. This can be absolute or relative to the current working directory.', 26 | }, 27 | l: { 28 | alias: ['locale', 'locales'], 29 | required: false, 30 | type: 'array', 31 | describe: 32 | 'The locale for the extracted file, "en" by default. If you use multiple locales (e.g. "en fr es"), a new file will be generated for each locale', 33 | }, 34 | }; 35 | 36 | export const handler = function (options) { 37 | const diagnostics = new Diagnostics(); 38 | extractTranslations({ 39 | sourceGlob: options['s'] as string, 40 | format: options['f'] as TranslationFormat, 41 | outputPath: options['o'] as string, 42 | locales: options['l'] as string[], 43 | diagnostics, 44 | }); 45 | diagnostics.logMessages(); 46 | process.exit(diagnostics.hasErrors ? 1 : 0); 47 | }; 48 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/extract/extract.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostics } from '../common/diagnostics'; 2 | import { FileUtils } from '../common/file_utils'; 3 | import { 4 | getExtension, 5 | getTranslationSerializer, 6 | TranslationFormat, 7 | } from '../common/util'; 8 | import { Extractor } from './extractor'; 9 | import { readFileSync, statSync } from 'fs'; 10 | import { sync } from 'glob'; 11 | import { basename, extname, posix, resolve } from 'path'; 12 | 13 | export interface ExtractTranslationsOptions { 14 | sourceGlob: string; 15 | format: TranslationFormat; 16 | outputPath: string; 17 | locales?: string[]; 18 | diagnostics: Diagnostics; 19 | } 20 | 21 | export function extractTranslations({ 22 | sourceGlob: source, 23 | format, 24 | outputPath: output, 25 | locales = ['en'], 26 | diagnostics, 27 | }: ExtractTranslationsOptions) { 28 | console.log(`Extracting translations from "${source}"`); 29 | let filesToProcess = sync(resolve(source), { 30 | absolute: true, 31 | nodir: true, 32 | }); 33 | filesToProcess = FileUtils.dedup(filesToProcess, /\-es(5|2015)\./, '.'); 34 | output = resolve(output); 35 | const generatedFiles: string[] = []; 36 | let isFile: boolean; 37 | try { 38 | const stat = statSync(output); 39 | isFile = stat.isFile(); 40 | } catch (e) { 41 | isFile = !!extname(output); 42 | } 43 | if (isFile) { 44 | if (locales.length > 1) { 45 | diagnostics.error( 46 | `Multiple locales detected ("${locales.join( 47 | ',' 48 | )}") but output "${output}" is not a directory` 49 | ); 50 | return; 51 | } 52 | const res = makeTranslationsFile( 53 | filesToProcess, 54 | posix.normalize(output), 55 | source, 56 | format, 57 | locales[0], 58 | diagnostics 59 | ); 60 | if (res) { 61 | generatedFiles.push(res); 62 | } 63 | } else { 64 | filesToProcess.forEach((file) => { 65 | locales.forEach((locale) => { 66 | const newFileName = posix.join( 67 | output, 68 | basename(file, '.js').replace(/-es(5|2015)/, '') + 69 | '.' + 70 | locale + 71 | '.' + 72 | getExtension(format) 73 | ); 74 | const res = makeTranslationsFile( 75 | [file], 76 | newFileName, 77 | source, 78 | format, 79 | locale, 80 | diagnostics 81 | ); 82 | if (res) { 83 | generatedFiles.push(res); 84 | } 85 | }); 86 | }); 87 | } 88 | if (!generatedFiles.length) { 89 | diagnostics.error( 90 | `No messages found. You should build the angular app without a language target for this command to work.` 91 | ); 92 | return; 93 | } 94 | } 95 | 96 | function makeTranslationsFile( 97 | filesToProcess: string[], 98 | fileOutput: string, 99 | source: string, 100 | format: TranslationFormat, 101 | locale: string, 102 | diagnostics: Diagnostics 103 | ): string | null { 104 | const extractor = new Extractor(diagnostics); 105 | filesToProcess.forEach((file) => { 106 | const contents = readFileSync(file, 'utf8'); 107 | extractor.extractMessages(contents); 108 | }); 109 | 110 | const serializer = getTranslationSerializer(format); 111 | if (extractor.messages.length > 0) { 112 | const translationFile = serializer.renderFile( 113 | extractor.messages, 114 | locale, 115 | false 116 | ); 117 | FileUtils.writeFile(fileOutput, translationFile); 118 | console.log(` Generated file "${fileOutput}"`); 119 | return fileOutput; 120 | } 121 | return null; 122 | } 123 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/extract/extractor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { ɵParsedMessage } from '@angular/localize'; 9 | import { transformSync } from '@babel/core'; 10 | 11 | import { makeEs2015ExtractPlugin } from './source_files/es2015_extract_plugin'; 12 | import { makeEs5ExtractPlugin } from './source_files/es5_extract_plugin'; 13 | import { Diagnostics } from '../common/diagnostics'; 14 | 15 | /** 16 | * A class that extracts parsed messages from file contents, by parsing the contents as JavaScript 17 | * and looking for occurrences of `$localize` in the source code. 18 | */ 19 | export class Extractor { 20 | messages: ɵParsedMessage[] = []; 21 | 22 | constructor(private diagnostics: Diagnostics) {} 23 | 24 | extractMessages(sourceCode: string): void { 25 | if (sourceCode.includes('$localize')) { 26 | // Only bother to parse the file if it contains a reference to `$localize`. 27 | transformSync(sourceCode, { 28 | plugins: [ 29 | makeEs2015ExtractPlugin(this.messages, this.diagnostics), 30 | makeEs5ExtractPlugin(this.messages, this.diagnostics) 31 | ], 32 | code: false, 33 | ast: false 34 | }); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/extract/source_files/es2015_extract_plugin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { ɵParsedMessage, ɵparseMessage } from '@angular/localize'; 9 | import { NodePath, PluginObj } from '@babel/core'; 10 | import { TaggedTemplateExpression } from '@babel/types'; 11 | import { ParsedMessageLegacy } from '../../../cmds/common/util'; 12 | 13 | import { 14 | isGlobalIdentifier, 15 | isNamedIdentifier, 16 | unwrapMessagePartsFromTemplateLiteral 17 | } from '../source_file_utils'; 18 | import { Diagnostics } from '../../common/diagnostics'; 19 | 20 | export function makeEs2015ExtractPlugin( 21 | messages: (ɵParsedMessage | ParsedMessageLegacy)[], 22 | diagnostics: Diagnostics, 23 | localizeName = '$localize' 24 | ): PluginObj { 25 | return { 26 | visitor: { 27 | TaggedTemplateExpression(path: NodePath) { 28 | const tag = path.get('tag'); 29 | if (isNamedIdentifier(tag, localizeName) && isGlobalIdentifier(tag)) { 30 | const messageParts = unwrapMessagePartsFromTemplateLiteral( 31 | path.node.quasi.quasis 32 | ); 33 | const message: ɵParsedMessage | ParsedMessageLegacy = ɵparseMessage( 34 | messageParts, 35 | path.node.quasi.expressions 36 | ); 37 | if ( 38 | !messages.find((msg: any) => 39 | message.id 40 | ? msg.id === message.id 41 | : msg.messageId === 42 | ((message) as ParsedMessageLegacy).messageId 43 | ) 44 | ) { 45 | messages.push(message); 46 | } 47 | } 48 | } 49 | } 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/extract/source_files/es5_extract_plugin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { ɵParsedMessage, ɵparseMessage } from '@angular/localize'; 9 | import { NodePath, PluginObj } from '@babel/core'; 10 | import { CallExpression } from '@babel/types'; 11 | import { ParsedMessageLegacy } from '../../../cmds/common/util'; 12 | 13 | import { 14 | isGlobalIdentifier, 15 | isNamedIdentifier, 16 | unwrapMessagePartsFromLocalizeCall, 17 | unwrapSubstitutionsFromLocalizeCall 18 | } from '../source_file_utils'; 19 | import { Diagnostics } from '../../common/diagnostics'; 20 | 21 | export function makeEs5ExtractPlugin( 22 | messages: (ɵParsedMessage | ParsedMessageLegacy)[], 23 | diagnostics: Diagnostics, 24 | localizeName = '$localize' 25 | ): PluginObj { 26 | return { 27 | visitor: { 28 | CallExpression(callPath: NodePath) { 29 | const calleePath = callPath.get('callee'); 30 | if ( 31 | isNamedIdentifier(calleePath, localizeName) && 32 | isGlobalIdentifier(calleePath) 33 | ) { 34 | const messageParts = unwrapMessagePartsFromLocalizeCall(callPath); 35 | const expressions = unwrapSubstitutionsFromLocalizeCall( 36 | callPath.node 37 | ); 38 | const message: ɵParsedMessage | ParsedMessageLegacy = ɵparseMessage( 39 | messageParts, 40 | expressions 41 | ); 42 | if ( 43 | !messages.find((msg: any) => 44 | message.id 45 | ? msg.id === message.id 46 | : msg.messageId === 47 | ((message) as ParsedMessageLegacy).messageId 48 | ) 49 | ) { 50 | messages.push(message); 51 | } 52 | } 53 | } 54 | } 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/extract/translation_files/json_translation_serializer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { ɵParsedMessage } from '@angular/localize'; 9 | import { ParsedMessageLegacy } from '../../../cmds/common/util'; 10 | import { TranslationSerializer } from './translation_serializer'; 11 | 12 | interface SimpleJsonTranslationFile { 13 | locale: string; 14 | translations: Record; 15 | } 16 | 17 | export class JsonTranslationSerializer implements TranslationSerializer { 18 | renderFile( 19 | messages: (ɵParsedMessage | ParsedMessageLegacy)[], 20 | locale: string 21 | ): string { 22 | const fileObj: SimpleJsonTranslationFile = { 23 | locale, 24 | translations: {} 25 | }; 26 | messages.forEach(message => { 27 | fileObj.translations[ 28 | (message as ɵParsedMessage).id || 29 | (message as ParsedMessageLegacy).messageId 30 | ] = 31 | (message as ɵParsedMessage).text || 32 | (message as ParsedMessageLegacy).messageString; 33 | }); 34 | return JSON.stringify(fileObj, null, 2); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/extract/translation_files/translation_serializer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { ɵParsedMessage } from '@angular/localize'; 9 | 10 | /** 11 | * Implement this interface to provide a class that can render messages into a translation file. 12 | */ 13 | export interface TranslationSerializer { 14 | /** 15 | * Render the contents of a translation file containing the given `messages`. 16 | * @param messages The messages to render to the file. 17 | */ 18 | renderFile( 19 | messages: ɵParsedMessage[], 20 | locale: string, 21 | isTarget?: boolean 22 | ): string; 23 | } 24 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/extract/translation_files/xliff1_translation_serializer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { ɵParsedMessage } from '@angular/localize'; 9 | import { ParsedMessageLegacy } from '../../../cmds/common/util'; 10 | import { TranslationSerializer } from './translation_serializer'; 11 | import { XmlFile } from './xml_file'; 12 | 13 | export class Xliff1TranslationSerializer implements TranslationSerializer { 14 | renderFile( 15 | messages: (ɵParsedMessage | ParsedMessageLegacy)[], 16 | locale: string, 17 | isTarget = false 18 | ): string { 19 | const xml = new XmlFile(); 20 | xml.startTag('xliff', { 21 | version: '1.2', 22 | xmlns: 'urn:oasis:names:tc:xliff:document:1.2' 23 | }); 24 | const fileAttrs = { 25 | ['source-language']: isTarget ? undefined : locale, 26 | ['target-language']: locale, 27 | datatype: 'plaintext' 28 | }; 29 | xml.startTag('file', fileAttrs); 30 | xml.startTag('body'); 31 | messages.forEach(message => { 32 | xml.startTag('trans-unit', { 33 | id: 34 | (message as ɵParsedMessage).id || 35 | (message as ParsedMessageLegacy).messageId, 36 | datatype: 'html' 37 | }); 38 | if (!isTarget) { 39 | this.generateMessageTag(xml, 'source', message); 40 | } 41 | this.generateMessageTag(xml, 'target', message); 42 | if (message.description) { 43 | this.renderNote(xml, 'description', message.description); 44 | } 45 | if (message.meaning) { 46 | this.renderNote(xml, 'meaning', message.meaning); 47 | } 48 | xml.endTag('trans-unit'); 49 | }); 50 | xml.endTag('body'); 51 | xml.endTag('file'); 52 | xml.endTag('xliff'); 53 | return xml.toString(); 54 | } 55 | 56 | private generateMessageTag( 57 | xml: XmlFile, 58 | tagName: string, 59 | message: ɵParsedMessage | ParsedMessageLegacy 60 | ) { 61 | xml.startTag(tagName, {}, { preserveWhitespace: true }); 62 | this.renderMessage(xml, message); 63 | xml.endTag(tagName, { preserveWhitespace: false }); 64 | } 65 | 66 | private renderMessage( 67 | xml: XmlFile, 68 | message: ɵParsedMessage | ParsedMessageLegacy 69 | ): void { 70 | xml.text(message.messageParts[0]); 71 | for (let i = 1; i < message.messageParts.length; i++) { 72 | xml.startTag( 73 | 'x', 74 | { id: message.placeholderNames[i - 1] }, 75 | { selfClosing: true } 76 | ); 77 | xml.text(message.messageParts[i]); 78 | } 79 | } 80 | 81 | private renderNote(xml: XmlFile, name: string, value: string) { 82 | xml.startTag( 83 | 'note', 84 | { priority: '1', from: name }, 85 | { preserveWhitespace: true } 86 | ); 87 | xml.text(value); 88 | xml.endTag('note', { preserveWhitespace: false }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/extract/translation_files/xliff2_translation_serializer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { ɵParsedMessage } from '@angular/localize'; 9 | import { ParsedMessageLegacy } from '../../../cmds/common/util'; 10 | import { TranslationSerializer } from './translation_serializer'; 11 | import { XmlFile } from './xml_file'; 12 | 13 | export class Xliff2TranslationSerializer implements TranslationSerializer { 14 | renderFile( 15 | messages: (ɵParsedMessage | ParsedMessageLegacy)[], 16 | locale: string, 17 | isTarget = false 18 | ): string { 19 | const xml = new XmlFile(); 20 | xml.startTag('xliff', { 21 | version: '2.0', 22 | xmlns: 'urn:oasis:names:tc:xliff:document:2.0', 23 | srcLang: locale, 24 | trgLang: locale, 25 | }); 26 | xml.startTag('file', { original: 'ng.template', id: 'locl' }); 27 | messages.forEach((message) => { 28 | xml.startTag('unit', { 29 | id: 30 | (message as ɵParsedMessage).id || 31 | (message as ParsedMessageLegacy).messageId, 32 | }); 33 | if (message.meaning || message.description) { 34 | xml.startTag('notes'); 35 | if (message.description) { 36 | this.renderNote(xml, 'description', message.description); 37 | } 38 | if (message.meaning) { 39 | this.renderNote(xml, 'meaning', message.meaning); 40 | } 41 | xml.endTag('notes'); 42 | } 43 | xml.startTag('segment'); 44 | if (!isTarget) { 45 | this.generateMessageTag(xml, 'source', message); 46 | } 47 | this.generateMessageTag(xml, 'target', message); 48 | xml.endTag('segment'); 49 | xml.endTag('unit'); 50 | }); 51 | xml.endTag('file'); 52 | xml.endTag('xliff'); 53 | return xml.toString(); 54 | } 55 | 56 | private generateMessageTag( 57 | xml: XmlFile, 58 | tagName: string, 59 | message: ɵParsedMessage | ParsedMessageLegacy 60 | ) { 61 | xml.startTag(tagName, {}, { preserveWhitespace: true }); 62 | this.renderMessage(xml, message); 63 | xml.endTag(tagName, { preserveWhitespace: false }); 64 | } 65 | 66 | private renderMessage( 67 | xml: XmlFile, 68 | message: ɵParsedMessage | ParsedMessageLegacy 69 | ): void { 70 | xml.text(message.messageParts[0]); 71 | for (let i = 1; i < message.messageParts.length; i++) { 72 | const placeholderName = message.placeholderNames[i - 1]; 73 | if (placeholderName.startsWith('START_')) { 74 | xml.startTag('pc', { 75 | id: `${i}`, 76 | equivStart: placeholderName, 77 | equivEnd: placeholderName.replace(/^START/, 'CLOSE'), 78 | }); 79 | } else if (placeholderName.startsWith('CLOSE_')) { 80 | xml.endTag('pc'); 81 | } else { 82 | xml.startTag( 83 | 'ph', 84 | { id: `${i}`, equiv: placeholderName }, 85 | { selfClosing: true } 86 | ); 87 | } 88 | xml.text(message.messageParts[i]); 89 | } 90 | } 91 | 92 | private renderNote(xml: XmlFile, name: string, value: string) { 93 | xml.startTag('note', { category: name }, { preserveWhitespace: true }); 94 | xml.text(value); 95 | xml.endTag('note', { preserveWhitespace: false }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/extract/translation_files/xmb_translation_serializer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { ɵParsedMessage } from '@angular/localize'; 9 | import { ParsedMessageLegacy } from '../../../cmds/common/util'; 10 | import { TranslationSerializer } from './translation_serializer'; 11 | import { XmlFile } from './xml_file'; 12 | 13 | const DOCTYPE = ` 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | `; 32 | 33 | export class XmbTranslationSerializer implements TranslationSerializer { 34 | renderFile(messages: (ɵParsedMessage | ParsedMessageLegacy)[]): string { 35 | const xml = new XmlFile(); 36 | xml.startTag('messagebundle'); 37 | messages.forEach(message => { 38 | xml.startTag( 39 | 'msg', 40 | { 41 | id: 42 | (message as ɵParsedMessage).id || 43 | (message as ParsedMessageLegacy).messageId, 44 | desc: message.description, 45 | meaning: message.meaning 46 | }, 47 | { preserveWhitespace: true } 48 | ); 49 | this.renderMessage(xml, message); 50 | xml.endTag('msg', { preserveWhitespace: false }); 51 | }); 52 | xml.endTag('messagebundle'); 53 | return xml.toString(); 54 | } 55 | 56 | private renderMessage( 57 | xml: XmlFile, 58 | message: ɵParsedMessage | ParsedMessageLegacy 59 | ): void { 60 | xml.text(message.messageParts[0]); 61 | for (let i = 1; i < message.messageParts.length; i++) { 62 | xml.startTag( 63 | 'ph', 64 | { name: message.placeholderNames[i - 1] }, 65 | { selfClosing: true } 66 | ); 67 | xml.text(message.messageParts[i]); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/extract/translation_files/xml_file.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | interface Options { 10 | selfClosing?: boolean; 11 | preserveWhitespace?: boolean; 12 | } 13 | 14 | export class XmlFile { 15 | private output = ''; 16 | private indent = ''; 17 | private elements: string[] = []; 18 | private preservingWhitespace = false; 19 | toString() { 20 | return this.output; 21 | } 22 | 23 | startTag( 24 | name: string, 25 | attributes: Record = {}, 26 | { selfClosing = false, preserveWhitespace }: Options = {} 27 | ): this { 28 | if (!this.preservingWhitespace) { 29 | this.output += this.indent; 30 | } 31 | 32 | this.output += `<${name}`; 33 | 34 | Object.keys(attributes).forEach(attrName => { 35 | if (attributes[attrName]) { 36 | this.output += ` ${attrName}="${escapeXml(attributes[attrName])}"`; 37 | } 38 | }); 39 | 40 | if (selfClosing) { 41 | this.output += '/>'; 42 | } else { 43 | this.output += '>'; 44 | this.elements.push(name); 45 | this.incIndent(); 46 | } 47 | 48 | if (preserveWhitespace !== undefined) { 49 | this.preservingWhitespace = preserveWhitespace; 50 | } 51 | if (!this.preservingWhitespace) { 52 | this.output += `\n`; 53 | } 54 | return this; 55 | } 56 | 57 | endTag(name: string, { preserveWhitespace }: Options = {}): this { 58 | const expectedTag = this.elements.pop(); 59 | if (expectedTag !== name) { 60 | throw new Error( 61 | `Unexpected closing tag: "${name}", expected: "${expectedTag}"` 62 | ); 63 | } 64 | 65 | this.decIndent(); 66 | 67 | if (!this.preservingWhitespace) { 68 | this.output += this.indent; 69 | } 70 | this.output += ``; 71 | 72 | if (preserveWhitespace !== undefined) { 73 | this.preservingWhitespace = preserveWhitespace; 74 | } 75 | if (!this.preservingWhitespace) { 76 | this.output += `\n`; 77 | } 78 | return this; 79 | } 80 | 81 | text(str: string): this { 82 | this.output += escapeXml(str); 83 | return this; 84 | } 85 | 86 | startPreserveWhitespace() { 87 | this.preservingWhitespace = true; 88 | } 89 | stopPreserveWhitespace() { 90 | this.preservingWhitespace = false; 91 | } 92 | 93 | private incIndent() { 94 | this.indent = this.indent + ' '; 95 | } 96 | private decIndent() { 97 | this.indent = this.indent.slice(0, -2); 98 | } 99 | } 100 | 101 | const _ESCAPED_CHARS: [RegExp, string][] = [ 102 | [/&/g, '&'], 103 | [/"/g, '"'], 104 | [/'/g, '''], 105 | [//g, '>'] 107 | ]; 108 | 109 | function escapeXml(text: string): string { 110 | return _ESCAPED_CHARS.reduce( 111 | (t: string, entry: [RegExp, string]) => t.replace(entry[0], entry[1]), 112 | text 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /libs/cli/src/cmds/extract/translation_files/xtb_translation_serializer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { ɵParsedMessage } from '@angular/localize'; 9 | import { ParsedMessageLegacy } from '../../../cmds/common/util'; 10 | import { TranslationSerializer } from './translation_serializer'; 11 | import { XmlFile } from './xml_file'; 12 | 13 | const DOCTYPE = ` 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ]> 29 | `; 30 | 31 | export class XtbTranslationSerializer implements TranslationSerializer { 32 | renderFile( 33 | messages: (ɵParsedMessage | ParsedMessageLegacy)[], 34 | locale: string 35 | ): string { 36 | const xml = new XmlFile(); 37 | xml.startTag('translationbundle', { lang: locale }); 38 | messages.forEach(message => { 39 | xml.startTag( 40 | 'translation', 41 | { 42 | id: 43 | (message as ɵParsedMessage).id || 44 | (message as ParsedMessageLegacy).messageId, 45 | desc: message.description, 46 | meaning: message.meaning 47 | }, 48 | { preserveWhitespace: true } 49 | ); 50 | this.renderMessage(xml, message); 51 | xml.endTag('translation', { preserveWhitespace: false }); 52 | }); 53 | xml.endTag('translationbundle'); 54 | return DOCTYPE + xml.toString(); 55 | } 56 | 57 | private renderMessage( 58 | xml: XmlFile, 59 | message: ɵParsedMessage | ParsedMessageLegacy 60 | ): void { 61 | xml.text(message.messageParts[0]); 62 | for (let i = 1; i < message.messageParts.length; i++) { 63 | xml.startTag( 64 | 'ph', 65 | { name: message.placeholderNames[i - 1] }, 66 | { selfClosing: true } 67 | ); 68 | xml.text(message.messageParts[i]); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /libs/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/cli.module'; 2 | export * from './cmds/extract/extract'; 3 | export * from './cmds/convert/convert'; 4 | export * from './main'; 5 | -------------------------------------------------------------------------------- /libs/cli/src/lib/cli.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | // not used, only there to make ng-packagr happy 4 | @NgModule({ 5 | imports: [], 6 | declarations: [], 7 | exports: [] 8 | }) 9 | export class CliModule {} 10 | -------------------------------------------------------------------------------- /libs/cli/src/locl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('./main.js'); -------------------------------------------------------------------------------- /libs/cli/src/main.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from 'yargs'; 2 | import * as findUp from 'find-up'; 3 | import * as fs from 'fs'; 4 | const configPath = findUp.sync(['.loclrc', '.locl.json']); 5 | const config = configPath 6 | ? JSON.parse(fs.readFileSync(configPath, { encoding: 'utf8' })) 7 | : {}; 8 | 9 | const args = process.argv.slice(2); 10 | yargs 11 | .config(config) 12 | .commandDir('cmds') 13 | .demandCommand() 14 | .strict() 15 | .help() 16 | .version(false) 17 | .parse(args); 18 | -------------------------------------------------------------------------------- /libs/cli/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; -------------------------------------------------------------------------------- /libs/cli/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.lib.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist", 5 | "lib": ["es2017", "dom"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noEmitOnError": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "noImplicitAny": false, 11 | "noImplicitThis": true, 12 | "noUnusedParameters": false, 13 | "noUnusedLocals": false, 14 | "skipDefaultLibCheck": true, 15 | "skipLibCheck": true, 16 | "strictNullChecks": false, 17 | "target": "es6" 18 | }, 19 | "include": ["**/*"], 20 | "exclude": [] 21 | } 22 | -------------------------------------------------------------------------------- /libs/cli/test/cmds/convert.spec.ts: -------------------------------------------------------------------------------- 1 | import { convertFiles } from '../../src/cmds/convert/convert'; 2 | import { Diagnostics } from '../../src/cmds/common/diagnostics'; 3 | import { 4 | mockFile, 5 | simpleTranslatedXlf, 6 | translatedJSON, 7 | translatedXlf, 8 | translatedXLF2, 9 | translatedXtb, 10 | } from './mock'; 11 | import { FileUtils } from '../../src/cmds/common/file_utils'; 12 | import { runInEachFileSystem } from '@angular/compiler-cli/src/ngtsc/file_system/testing'; 13 | 14 | runInEachFileSystem(() => { 15 | describe('convertFiles', () => { 16 | it('should convert xlf to json', function () { 17 | mockFile(['fr.xlf', '/i18n/fr.json'], [translatedXlf]); 18 | const spy = spyOn(FileUtils, 'writeFile'); 19 | 20 | convertFiles({ 21 | format: 'json', 22 | sourceGlob: 'fr.xlf', 23 | outputPath: '/i18n/fr.json', 24 | diagnostics: new Diagnostics(), 25 | }); 26 | 27 | expect(spy).toHaveBeenCalledWith('/i18n/fr.json', translatedJSON); 28 | }); 29 | 30 | it('should convert json to xlf', function () { 31 | mockFile(['fr.json', '/i18n/fr.xlf'], [translatedJSON]); 32 | const spy = spyOn(FileUtils, 'writeFile'); 33 | 34 | convertFiles({ 35 | format: 'xlf', 36 | sourceGlob: 'fr.json', 37 | outputPath: '/i18n/fr.xlf', 38 | diagnostics: new Diagnostics(), 39 | }); 40 | 41 | expect(spy).toHaveBeenCalledWith('/i18n/fr.xlf', simpleTranslatedXlf); 42 | }); 43 | 44 | it('should convert xlf to xtb', function () { 45 | mockFile(['fr.xlf', '/i18n/fr.xtb'], [translatedXlf]); 46 | const spy = spyOn(FileUtils, 'writeFile'); 47 | 48 | convertFiles({ 49 | format: 'xtb', 50 | sourceGlob: 'fr.xlf', 51 | outputPath: '/i18n/fr.xtb', 52 | diagnostics: new Diagnostics(), 53 | }); 54 | 55 | expect(spy).toHaveBeenCalledWith('/i18n/fr.xtb', translatedXtb); 56 | }); 57 | 58 | it('should convert xtb to xlf2', function () { 59 | mockFile(['fr.xtb', '/i18n/fr.xlf'], [translatedXtb]); 60 | const spy = spyOn(FileUtils, 'writeFile'); 61 | 62 | convertFiles({ 63 | format: 'xlf2', 64 | sourceGlob: 'fr.xtb', 65 | outputPath: '/i18n/fr.xlf', 66 | diagnostics: new Diagnostics(), 67 | }); 68 | 69 | expect(spy).toHaveBeenCalledWith('/i18n/fr.xlf', translatedXLF2); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /libs/cli/test/cmds/extract.spec.ts: -------------------------------------------------------------------------------- 1 | import { runInEachFileSystem } from '@angular/compiler-cli/src/ngtsc/file_system/testing'; 2 | import { Diagnostics } from '../../src/cmds/common/diagnostics'; 3 | import { FileUtils } from '../../src/cmds/common/file_utils'; 4 | import { extractTranslations } from '../../src/cmds/extract/extract'; 5 | import { mockFile, targetXlf } from './mock'; 6 | 7 | runInEachFileSystem(() => { 8 | describe('extract', () => { 9 | it('should work', () => { 10 | mockFile(['main-es2015.js', '/i18/en.xlf'], [mainEs2015]); 11 | const spy = spyOn(FileUtils, 'writeFile'); 12 | 13 | extractTranslations({ 14 | format: 'xlf', 15 | sourceGlob: 'main-es2015.js', 16 | outputPath: '/i18/en.xlf', 17 | diagnostics: new Diagnostics(), 18 | }); 19 | 20 | expect(spy).toHaveBeenCalledWith('/i18/en.xlf', targetXlf('en', false)); 21 | }); 22 | 23 | it('should create files automatically if output is a folder', () => { 24 | mockFile(['main-es2015.js', '/i18'], [mainEs2015]); 25 | const spy = spyOn(FileUtils, 'writeFile'); 26 | 27 | extractTranslations({ 28 | format: 'xlf', 29 | sourceGlob: 'main-es2015.js', 30 | outputPath: '/i18', 31 | diagnostics: new Diagnostics(), 32 | }); 33 | 34 | expect(spy).toHaveBeenCalledWith( 35 | '/i18/main.en.xlf', 36 | targetXlf('en', false) 37 | ); 38 | }); 39 | 40 | it('should create multiple files if output is a folder and locales is an array', () => { 41 | mockFile(['main-es2015.js', '/i18'], [mainEs2015]); 42 | const spy = spyOn(FileUtils, 'writeFile'); 43 | 44 | extractTranslations({ 45 | format: 'xlf', 46 | sourceGlob: 'main-es2015.js', 47 | outputPath: '/i18', 48 | locales: ['en', 'fr'], 49 | diagnostics: new Diagnostics(), 50 | }); 51 | 52 | expect(spy).toHaveBeenCalledTimes(2); 53 | expect(spy).toHaveBeenNthCalledWith( 54 | 1, 55 | '/i18/main.en.xlf', 56 | targetXlf('en', false) 57 | ); 58 | expect(spy).toHaveBeenNthCalledWith( 59 | 2, 60 | '/i18/main.fr.xlf', 61 | targetXlf('fr', false) 62 | ); 63 | }); 64 | }); 65 | }); 66 | 67 | const mainEs2015 = ` 68 | var I18N_0; 69 | if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) { 70 | const MSG_EXTERNAL_5105652583545451042$$APPS_DEMO_SRC_APP_APP_COMPONENT_TS_1 = goog.getMsg("It works!"); 71 | I18N_0 = MSG_EXTERNAL_5105652583545451042$$APPS_DEMO_SRC_APP_APP_COMPONENT_TS_1; 72 | } 73 | else { 74 | I18N_0 = $localize \`:site header|An introduction header for this sample␟7e1a20ccc16692f73c6e224b489dc7e275ecc6ed␟3987846127133982403:It works!\`; 75 | } 76 | const name = '$localize'; 77 | const lib = 'Locl'; 78 | class AppComponent { 79 | constructor() { 80 | this.title = $localize \`Welcome to the demo of \${name} and \${lib} made for \${name}!\`; 81 | console.log($localize \`:@@foo:custom id!\`); 82 | } 83 | }`; 84 | -------------------------------------------------------------------------------- /libs/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "node", 6 | "jest" 7 | ] 8 | }, 9 | "include": [ 10 | "**/*.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /libs/cli/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": [ 10 | "dom", 11 | "es2018" 12 | ] 13 | }, 14 | "angularCompilerOptions": { 15 | "annotateForClosureCompiler": true, 16 | "skipTemplateCodegen": true, 17 | "strictMetadataEmit": true, 18 | "fullTemplateTypeCheck": true, 19 | "strictInjectionParameters": true, 20 | "enableResourceInlining": true, 21 | "enableIvy": false 22 | }, 23 | "exclude": [ 24 | "src/test.ts", 25 | "**/*.spec.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /libs/cli/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": [ 7 | "jest", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "src/test-setup.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/cli/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "locl", "camelCase"], 5 | "component-selector": [true, "element", "locl", "kebab-case"] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["!**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/common/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/common", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@locl/common", 3 | "version": "0.0.1", 4 | "peerDependencies": { 5 | "@angular/core": ">=9.0.0-next.5" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/common.module'; 2 | -------------------------------------------------------------------------------- /libs/common/src/lib/common.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { LoclCommonModule } from './common.module'; 3 | 4 | describe('CommonModule', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | imports: [LoclCommonModule] 8 | }).compileComponents(); 9 | })); 10 | 11 | it('should create', () => { 12 | expect(LoclCommonModule).toBeDefined(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /libs/common/src/lib/common.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | @NgModule({ 4 | imports: [] 5 | }) 6 | export class LoclCommonModule { 7 | } 8 | -------------------------------------------------------------------------------- /libs/common/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; -------------------------------------------------------------------------------- /libs/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "node", 6 | "jest" 7 | ] 8 | }, 9 | "include": [ 10 | "**/*.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /libs/common/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": [ 10 | "dom", 11 | "es2018" 12 | ] 13 | }, 14 | "angularCompilerOptions": { 15 | "annotateForClosureCompiler": true, 16 | "skipTemplateCodegen": true, 17 | "strictMetadataEmit": true, 18 | "fullTemplateTypeCheck": true, 19 | "strictInjectionParameters": true, 20 | "enableResourceInlining": true, 21 | "enableIvy": false 22 | }, 23 | "exclude": [ 24 | "src/test.ts", 25 | "**/*.spec.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /libs/common/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": [ 7 | "jest", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "src/test-setup.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/common/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "locl", "camelCase"], 5 | "component-selector": [true, "element", "locl", "kebab-case"] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["!**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/core/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | # Locl Core 4 | 5 | A library with various utility functions to help you with `$localize` (if you need help to get started with `$localize`, [read this](https://blog.ninja-squad.com/2019/12/10/angular-localize/)). 6 | 7 | [Demo on StackBlitz](https://stackblitz.com/edit/ivy-ovy4cd) (it can take a long time to load the first time because ivy support on StackBlitz is still WIP). 8 | 9 | ## Installation 10 | 11 | Install the core library with npm: 12 | 13 | ```sh 14 | npm install @locl/core --save 15 | ``` 16 | 17 | ## Usage 18 | 19 | ### Loading the translations at runtime 20 | 21 | Angular translates the templates as soon as the components are loaded, which means that if you want to load the translations at runtime, you need to load them before the application starts. 22 | The best way to do that, is to load the translations before `bootstrapModule` gets called. 23 | 24 | `@locl/core` provides two functions to help you get the files over http: 25 | 26 | - `getTranslations(url: string, method?: 'GET'|'POST', headers?: {[key: string]: string}, async?: boolean): Promise`: Gets a translation file from a server using an XHR HTTP request 27 | - `fetchTranslations(url: string, method?: 'GET'|'POST', headers?: {[key: string]: string}): Promise`: Gets a translation file from a server using the fetch API (see [browser compatibility for the fetch API on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)) 28 | 29 | Both methods return a promise with a `ParsedTranslationBundle` object containing the translations and the locale. Common usage is the following: 30 | 31 | ```ts 32 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 33 | import { getTranslations, ParsedTranslationBundle } from '@locl/core'; 34 | import { AppModule } from './app/app.module'; 35 | 36 | getTranslations('/assets/i18n/fr.json').then( 37 | (data: ParsedTranslationBundle) => { 38 | platformBrowserDynamic() 39 | .bootstrapModule(AppModule) 40 | .catch(err => console.error(err)); 41 | } 42 | ); 43 | ``` 44 | 45 | - `loadTranslations(parsedTranslationBundle: ParsedTranslationBundle)`: Finishes initialization of \$localize, loads translations in memory and sets the `LOCALE_ID` value. 46 | Use this **only** if you're not using any of the two functions `getTranslations` or `fetchTranslations`. 47 | 48 | ### Util functions 49 | 50 | - `getBrowserLang(): string`: Returns the current browser lang (e.g. "fr") if available, or an empty string otherwise 51 | - `getBrowserCultureLang(): string`: Returns the current browser culture language name (e.g. "fr-FR") if available, or an empty string otherwise 52 | -------------------------------------------------------------------------------- /libs/core/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/core", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@locl/core", 3 | "version": "1.0.1", 4 | "license": "AGPL-3.0-or-later", 5 | "peerDependencies": { 6 | "@angular/core": "^10.0.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libs/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/core.module'; 2 | export * from './lib/init'; 3 | -------------------------------------------------------------------------------- /libs/core/src/lib/core.module.ts: -------------------------------------------------------------------------------- 1 | import { ɵMessageId, ɵTargetMessage } from '@angular/localize'; 2 | import { loadTranslations } from './init'; 3 | 4 | export function parseTranslations( 5 | fileContent: string 6 | ): ParsedTranslationBundle { 7 | let data: ParsedTranslationBundle; 8 | 9 | // Test if the content is json 10 | if ( 11 | /^[\],:{}\s]*$/.test( 12 | fileContent 13 | .replace(/\\["\\\/bfnrtu]/g, '@') 14 | .replace( 15 | /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, 16 | ']' 17 | ) 18 | .replace(/(?:^|:|,)(?:\s*\[)+/g, '') 19 | ) 20 | ) { 21 | data = JSON.parse(fileContent); 22 | } else { 23 | throw new Error('Only JSON translation files are supported at the moment'); 24 | } 25 | return data; 26 | } 27 | 28 | /** 29 | * Gets a translation file from a server using an XHR HTTP request 30 | * @param url 31 | * @param method the method used to get the translations, either `GET` or `POST` 32 | * @param headers an object containing a list of header/value to set for the XHR request 33 | * @param async defines if the XHR request should be async (default) or not 34 | */ 35 | export function getTranslations( 36 | url, 37 | method: 'GET' | 'POST' = 'GET', 38 | headers: { [key: string]: string } = {}, 39 | async = true 40 | ): Promise { 41 | return new Promise((resolve, reject) => { 42 | const xhr = new XMLHttpRequest(); 43 | 44 | xhr.onload = function() { 45 | try { 46 | const res = parseTranslations(xhr.responseText); 47 | loadTranslations(res); 48 | resolve(res); 49 | } catch (e) { 50 | reject(e); 51 | } 52 | }; 53 | 54 | xhr.onerror = function() { 55 | reject(new Error('Network request failed')); 56 | }; 57 | 58 | xhr.ontimeout = function() { 59 | reject(new Error('Network request timeout')); 60 | }; 61 | 62 | xhr.onabort = function() { 63 | reject(new Error('Network request aborted')); 64 | }; 65 | 66 | Object.keys(headers).forEach(key => 67 | xhr.setRequestHeader(key, headers[key]) 68 | ); 69 | xhr.open(method, url, async); 70 | xhr.send(); 71 | }); 72 | } 73 | 74 | export interface ParsedTranslationBundle { 75 | locale: string; 76 | translations: Record<ɵMessageId, ɵTargetMessage>; 77 | } 78 | 79 | /** 80 | * Gets a translation file from a server using the fetch API 81 | * @param url 82 | * @param method the method used to get the translations, either `GET` or `POST` 83 | * @param headers an object containing a list of header/value to set for the XHR request 84 | */ 85 | export function fetchTranslations( 86 | url: string, 87 | method: 'GET' | 'POST' = 'GET', 88 | headers: { [key: string]: string } = {} 89 | ): Promise { 90 | return fetch(url, { method, headers }) 91 | .then(response => response.text()) 92 | .then((response: string) => { 93 | const res = parseTranslations(response); 94 | loadTranslations(res); 95 | return res; 96 | }); 97 | } 98 | 99 | /** 100 | * Returns the language code name from the browser, e.g. "fr" 101 | */ 102 | export function getBrowserLang(): string { 103 | if ( 104 | typeof window === 'undefined' || 105 | typeof window.navigator === 'undefined' 106 | ) { 107 | return ''; 108 | } 109 | 110 | return getBrowserCultureLang().split('-')[0]; 111 | } 112 | 113 | /** 114 | * Returns the culture language code name from the browser, e.g. "fr-FR" 115 | */ 116 | export function getBrowserCultureLang(): string { 117 | if ( 118 | typeof window === 'undefined' || 119 | typeof window.navigator === 'undefined' 120 | ) { 121 | return ''; 122 | } 123 | 124 | let browserCultureLang = window.navigator.languages 125 | ? window.navigator.languages[0] 126 | : null; 127 | browserCultureLang = 128 | browserCultureLang || 129 | window.navigator.language || 130 | (window.navigator as any).browserLanguage || 131 | (window.navigator as any).userLanguage; 132 | 133 | return browserCultureLang.replace(/_/g, '-'); 134 | } 135 | -------------------------------------------------------------------------------- /libs/core/src/lib/global.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | // ********************************************************************************************** 10 | // This code to access the global object is mostly copied from `packages/core/src/util/global.ts` 11 | 12 | declare global { 13 | var WorkerGlobalScope: any; 14 | } 15 | 16 | const __globalThis = typeof globalThis !== 'undefined' && globalThis; 17 | const __window = typeof window !== 'undefined' && window; 18 | const __self = 19 | typeof self !== 'undefined' && 20 | typeof WorkerGlobalScope !== 'undefined' && 21 | self instanceof WorkerGlobalScope && 22 | self; 23 | // Always use __globalThis if available; this is the spec-defined global variable across all 24 | // environments. 25 | // Then fallback to __global first; in Node tests both __global and __window may be defined. 26 | export const _global: any = __globalThis || __window || __self; 27 | -------------------------------------------------------------------------------- /libs/core/src/lib/init.ts: -------------------------------------------------------------------------------- 1 | // Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. 2 | import '@angular/localize/init'; 3 | import { ɵɵi18nPostprocess } from '@angular/core'; 4 | import { loadTranslations as _loadTranslations } from '@angular/localize'; 5 | import { ParsedTranslationBundle } from './interfaces'; 6 | import { _global } from './global'; 7 | 8 | /** 9 | * The $locl function is an emulation of $localize but it returns a `LoclString` 10 | * instead of a real string. A `LoclString` is only evaluated when the template 11 | * is created, not just when the file containing that template is loaded by the 12 | * browser (at bootstrap). This means that we can delay initialization of the 13 | * string values and wait for the translations to be loaded. 14 | * 15 | * @param messageParts 16 | * @param expressions 17 | */ 18 | export const $locl = function ( 19 | messageParts: TemplateStringsArray, 20 | ...expressions: readonly any[] 21 | ) { 22 | return new LoclString(messageParts, ...expressions); 23 | }; 24 | 25 | // keep a local backup the of the real $localize 26 | const backUpLocalize = _global.$localize; 27 | // override $localize until everything is loaded 28 | _global.$localize = $locl; 29 | let isLoaded = false; 30 | 31 | class LoclString extends String { 32 | private readonly initParams: [TemplateStringsArray, ...(readonly any[])]; 33 | private value: string = null; 34 | private postProcess = false; 35 | 36 | constructor( 37 | messageParts: TemplateStringsArray, 38 | ...expressions: readonly any[] 39 | ) { 40 | super(); 41 | this.initParams = [messageParts, ...expressions]; 42 | } 43 | 44 | toString(): string { 45 | if (this.value) { 46 | return this.value; 47 | } 48 | if (!isLoaded) { 49 | return backUpLocalize(...this.initParams); 50 | } 51 | this.value = _global.$localize(...this.initParams); 52 | if (this.postProcess) { 53 | this.value = ɵɵi18nPostprocess(this.value); 54 | this.postProcess = false; 55 | } 56 | return this.value; 57 | } 58 | 59 | valueOf(): string { 60 | return this.toString(); 61 | } 62 | 63 | replace(search: any, replaceValue: any): string { 64 | this.postProcess = true; 65 | return this as any; 66 | } 67 | } 68 | 69 | /** 70 | * Finishes initialization of $localize, loads translations in memory and sets 71 | * the `LOCALE_ID` value. 72 | * Use this **only** if you're not using any of the two functions 73 | * `getTranslations` or `fetchTranslations`. 74 | */ 75 | export function loadTranslations( 76 | parsedTranslationBundle?: ParsedTranslationBundle 77 | ) { 78 | // Restore $localize 79 | _global.$localize = backUpLocalize; 80 | if ( 81 | parsedTranslationBundle?.translations && 82 | Object.keys(parsedTranslationBundle.translations).length 83 | ) { 84 | _loadTranslations(parsedTranslationBundle.translations); 85 | } 86 | if (parsedTranslationBundle?.locale) { 87 | _global.$localize.locale = parsedTranslationBundle.locale; 88 | } 89 | isLoaded = true; 90 | } 91 | -------------------------------------------------------------------------------- /libs/core/src/lib/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { ɵMessageId, ɵTargetMessage } from '@angular/localize'; 2 | 3 | export interface ParsedTranslationBundle { 4 | locale: string; 5 | translations: Record<ɵMessageId, ɵTargetMessage>; 6 | } 7 | -------------------------------------------------------------------------------- /libs/core/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; -------------------------------------------------------------------------------- /libs/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "node", 6 | "jest" 7 | ] 8 | }, 9 | "include": [ 10 | "**/*.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /libs/core/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": [ 10 | "dom", 11 | "es2018" 12 | ] 13 | }, 14 | "angularCompilerOptions": { 15 | "annotateForClosureCompiler": true, 16 | "skipTemplateCodegen": true, 17 | "strictMetadataEmit": true, 18 | "fullTemplateTypeCheck": true, 19 | "strictInjectionParameters": true, 20 | "enableResourceInlining": true, 21 | "enableIvy": false 22 | }, 23 | "exclude": [ 24 | "src/test.ts", 25 | "**/*.spec.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /libs/core/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": [ 7 | "jest", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "src/test-setup.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/core/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "locl", "camelCase"], 5 | "component-selector": [true, "element", "locl", "kebab-case"] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["!**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/scss/_index.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * The following are shared across all platforms and apps 3 | */ 4 | // shared variables 5 | @import 'variables'; 6 | body { 7 | background-color: #fff; 8 | } 9 | // create/import other scss files or define as needed... 10 | -------------------------------------------------------------------------------- /libs/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Shared across all platforms and apps 3 | * You may define a set of global variables accessible across entire workspace here 4 | */ 5 | 6 | // could use a base theme here, for example: 7 | $radius: 2px; 8 | 9 | /* Colors: https://coolors.co/ffc338-ba2727-0874bd-373734-f0f4ed */ 10 | $red: #ba2727; 11 | $yellow: #ffc338; 12 | $orange: #ffa814; 13 | $green: #83b800; 14 | $blue: #0874bd; 15 | $cyan: #209CEE; 16 | $black: #1c1c1c; 17 | $grey-darker: #373a4c; 18 | $grey-dark: #373734; 19 | $grey-light: #f0f4ed; 20 | $beige-light: #D0D1CD; 21 | $beige-lighter: #EFF0EB; 22 | 23 | $primary: $blue; 24 | $secondary: $green; 25 | $background: $grey-dark; 26 | $content-bg: #fff; 27 | $btn-bg: $grey-dark; 28 | $btn-color: #fff; 29 | 30 | $text-color: $black; 31 | $text: $grey-dark; 32 | $link: $blue; 33 | $info: $cyan; 34 | $success: $green; 35 | $warning: $yellow; 36 | $danger: $red; 37 | $dark: $grey-darker; 38 | 39 | $input-background-color: $beige-lighter; 40 | $input-shadow: none; -------------------------------------------------------------------------------- /libs/scss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@locl/scss", 3 | "version": "1.0.0", 4 | "main": "_index.scss" 5 | } 6 | -------------------------------------------------------------------------------- /libs/utils/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/utils", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@locl/utils", 3 | "version": "0.0.1" 4 | } 5 | -------------------------------------------------------------------------------- /libs/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/angular'; 2 | export * from './lib/objects'; 3 | export * from './lib/platform'; 4 | -------------------------------------------------------------------------------- /libs/utils/src/lib/angular.ts: -------------------------------------------------------------------------------- 1 | export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) { 2 | if (parentModule) { 3 | throw new Error( 4 | `${moduleName} has already been loaded. Import ${moduleName} in the AppModule only.` 5 | ); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/utils/src/lib/objects.ts: -------------------------------------------------------------------------------- 1 | export const isString = function(arg: any) { 2 | return typeof arg === 'string'; 3 | }; 4 | 5 | export const isObject = function(arg: any) { 6 | return arg && typeof arg === 'object'; 7 | }; 8 | -------------------------------------------------------------------------------- /libs/utils/src/lib/platform.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NativeScript helpers 3 | */ 4 | 5 | declare var NSObject: any, NSString: any, android: any, java: any, window: any; 6 | 7 | /** 8 | * Determine if running on native iOS mobile app 9 | */ 10 | export function isIOS() { 11 | return typeof NSObject !== 'undefined' && typeof NSString !== 'undefined'; 12 | } 13 | 14 | /** 15 | * Determine if running on native Android mobile app 16 | */ 17 | export function isAndroid() { 18 | return typeof android !== 'undefined' && typeof java !== 'undefined'; 19 | } 20 | 21 | /** 22 | * Determine if running on native iOS or Android mobile app 23 | */ 24 | export function isNativeScript() { 25 | return isIOS() || isAndroid(); 26 | } 27 | 28 | /** 29 | * Electron helpers 30 | */ 31 | export function isElectron() { 32 | return typeof window !== 'undefined' && window.process && window.process.type; 33 | } 34 | -------------------------------------------------------------------------------- /libs/utils/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; -------------------------------------------------------------------------------- /libs/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "node", 6 | "jest" 7 | ] 8 | }, 9 | "include": [ 10 | "**/*.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /libs/utils/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": [ 10 | "dom", 11 | "es2018" 12 | ] 13 | }, 14 | "angularCompilerOptions": { 15 | "annotateForClosureCompiler": true, 16 | "skipTemplateCodegen": true, 17 | "strictMetadataEmit": true, 18 | "fullTemplateTypeCheck": true, 19 | "strictInjectionParameters": true, 20 | "enableResourceInlining": true, 21 | "enableIvy": false 22 | }, 23 | "exclude": [ 24 | "src/test.ts", 25 | "**/*.spec.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /libs/utils/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": [ 7 | "jest", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "src/test-setup.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/utils/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "locl", "camelCase"], 5 | "component-selector": [true, "element", "locl", "kebab-case"] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["!**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "locl", 3 | "implicitDependencies": { 4 | "angular.json": "*", 5 | "package.json": "*", 6 | "tsconfig.json": "*", 7 | "tslint.json": "*", 8 | "nx.json": "*" 9 | }, 10 | "projects": { 11 | "web-e2e": { 12 | "tags": [] 13 | }, 14 | "web": { 15 | "tags": ["web"] 16 | }, 17 | "electron": { 18 | "tags": ["electron"] 19 | }, 20 | "core": { 21 | "tags": ["core"] 22 | }, 23 | "common": { 24 | "tags": ["common"] 25 | }, 26 | "utils": { 27 | "tags": ["utils"] 28 | }, 29 | "cli": { 30 | "tags": ["cli"] 31 | }, 32 | "app": { 33 | "tags": ["app:web", "app:electron"] 34 | }, 35 | "demo-e2e": { 36 | "tags": [] 37 | }, 38 | "demo": { 39 | "tags": [] 40 | } 41 | }, 42 | "tasksRunnerOptions": { 43 | "default": { 44 | "runner": "@nrwl/workspace/tasks-runners/default", 45 | "options": { 46 | "cacheableOperations": ["build", "lint", "test", "e2e"] 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /testing/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /testing/test.libs.ts: -------------------------------------------------------------------------------- 1 | import { getTestBed } from '@angular/core/testing'; 2 | import { 3 | BrowserDynamicTestingModule, 4 | platformBrowserDynamicTesting 5 | } from '@angular/platform-browser-dynamic/testing'; 6 | import 'zone.js/dist/zone'; 7 | import 'zone.js/dist/zone-testing'; 8 | 9 | declare const require: any; 10 | 11 | // First, initialize the Angular testing environment. 12 | getTestBed().initTestEnvironment( 13 | BrowserDynamicTestingModule, 14 | platformBrowserDynamicTesting() 15 | ); 16 | // Then we find all the tests. 17 | const contextLibs = require.context('../libs', true, /\.spec\.ts$/); 18 | // And load the modules. 19 | contextLibs.keys().map(contextLibs); 20 | -------------------------------------------------------------------------------- /testing/test.xplat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Test all shared code across /libs and /xplat 3 | * Split out to allow targeted testing against /libs or various xplat support folders 4 | */ 5 | import './test.libs'; 6 | 7 | declare const require: any; 8 | 9 | // web support only right now 10 | const contextWeb = require.context('../xplat/web', true, /\.spec\.ts$/); 11 | // And load the modules. 12 | contextWeb.keys().map(contextWeb); 13 | -------------------------------------------------------------------------------- /testing/tsconfig.libs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/libs", 5 | "target": "es2015", 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "types": [], 15 | "lib": [ 16 | "dom", 17 | "es2015" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "annotateForClosureCompiler": true, 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true, 26 | "flatModuleId": "AUTOGENERATED", 27 | "flatModuleOutFile": "AUTOGENERATED" 28 | }, 29 | "exclude": [ 30 | "test.libs.ts", 31 | "**/*.spec.ts" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /testing/tsconfig.libs.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/libs", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.libs.ts" 12 | ], 13 | "include": [ 14 | "../libs/**/*.spec.ts", 15 | "../libs/**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /testing/tsconfig.xplat.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.libs.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/xplat" 5 | }, 6 | "exclude": [ 7 | "test.xplat.ts", 8 | "**/*.spec.ts" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /testing/tsconfig.xplat.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.libs.spec.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/xplat" 5 | }, 6 | "files": [ 7 | "test.xplat.ts" 8 | ], 9 | "include": [ 10 | "../libs/**/*.spec.ts", 11 | "../libs/**/*.d.ts", 12 | "../xplat/web/**/*.spec.ts", 13 | "../xplat/web/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tools/commitlint-plugin-body-content/commitlint-plugin-body-content.js: -------------------------------------------------------------------------------- 1 | const message = require('@commitlint/message'); 2 | 3 | function getErrorMsg(negated, value, types) { 4 | return message.default([ 5 | 'body', 6 | negated ? 'may not' : 'must', 7 | `contain [${value}] for scopes [${types.join(', ')}]`, 8 | ]); 9 | } 10 | 11 | module.exports = { 12 | rules: { 13 | 'body-content': function (parsed, when, params) { 14 | const body = parsed.body; 15 | const footer = parsed.footer; 16 | const type = parsed.type; 17 | const negated = when === 'never'; 18 | const value = params[0]; 19 | const types = params[1]; 20 | if (!body && !footer) { 21 | if (negated || types.indexOf(type) === -1) { 22 | return [true]; 23 | } else { 24 | return [false, getErrorMsg(negated, value, types)]; 25 | } 26 | } 27 | 28 | const hasRegexp = 29 | (body 30 | ? body.split(/[\r\n]+/).find((str) => str.search(value) !== -1) 31 | : false) || 32 | (footer 33 | ? footer.split(/[\r\n]+/).find((str) => str.search(value) !== -1) 34 | : false); 35 | 36 | return [ 37 | negated ? !hasRegexp : hasRegexp, 38 | getErrorMsg(negated, value, types), 39 | ]; 40 | }, 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /tools/commitlint-plugin-body-content/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commitlint-plugin-body-content", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "commitlint-plugin-body-content.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "commitlint", 11 | "commitlintplugin" 12 | ], 13 | "author": "", 14 | "license": "ISC", 15 | "peerDependencies": { 16 | "@commitlint/lint": ">=7.6.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tools/electron/postinstall.js: -------------------------------------------------------------------------------- 1 | // ALL CREDIT TO: Maxime GRIS https://github.com/maximegris 2 | // Allow angular using electron module (native node modules) 3 | const fs = require('fs'); 4 | const f_angular = 5 | 'node_modules/@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js'; 6 | 7 | fs.readFile(f_angular, 'utf8', function(err, data) { 8 | if (err) { 9 | return console.log(err); 10 | } 11 | var result = data.replace(/target: "electron-renderer",/g, ''); 12 | var result = result.replace(/target: "web",/g, ''); 13 | var result = result.replace( 14 | /return \{/g, 15 | 'return {target: "electron-renderer",' 16 | ); 17 | 18 | fs.writeFile(f_angular, result, 'utf8', function(err) { 19 | if (err) return console.log(err); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tools/schematics/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loclapp/locl/71001b1321841738cecfeafee304f1fb4e686e87/tools/schematics/.gitkeep -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "node" 10 | ] 11 | }, 12 | "include": [ 13 | "**/*.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tools/web/postinstall.js: -------------------------------------------------------------------------------- 1 | // ALL CREDIT TO: Maxime GRIS https://github.com/maximegris 2 | // Allow angular using electron module (native node modules) 3 | const fs = require('fs'); 4 | const f_angular = 5 | 'node_modules/@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js'; 6 | 7 | fs.readFile(f_angular, 'utf8', function(err, data) { 8 | if (err) { 9 | return console.log(err); 10 | } 11 | var result = data.replace(/target: "electron-renderer",/g, ''); 12 | var result = result.replace(/target: "web",/g, ''); 13 | var result = result.replace(/return \{/g, 'return {target: "web",'); 14 | 15 | fs.writeFile(f_angular, result, 'utf8', function(err) { 16 | if (err) return console.log(err); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2017", 18 | "dom" 19 | ], 20 | "skipLibCheck": true, 21 | "skipDefaultLibCheck": true, 22 | "baseUrl": ".", 23 | "paths": { 24 | "@locl/*": [ 25 | "libs/*" 26 | ], 27 | "@locl/core": [ 28 | "libs/core/src/index.ts" 29 | ], 30 | "@locl/utils": [ 31 | "libs/utils/src/index.ts" 32 | ], 33 | "@locl/common": [ 34 | "libs/common/src/index.ts" 35 | ], 36 | "@locl/cli": [ 37 | "libs/cli/src/index.ts" 38 | ], 39 | "@locl/app-web": [ 40 | "libs/app/src/web/index.ts" 41 | ], 42 | "@locl/app-electron": [ 43 | "libs/app/src/electron/index.ts" 44 | ] 45 | } 46 | }, 47 | "exclude": [ 48 | "node_modules", 49 | "tmp" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/@nrwl/workspace/src/tslint", 4 | "node_modules/codelyzer" 5 | ], 6 | "rules": { 7 | "arrow-return-shorthand": true, 8 | "callable-types": true, 9 | "class-name": true, 10 | "deprecation": { 11 | "severity": "warn" 12 | }, 13 | "import-blacklist": [true, "rxjs/Rx"], 14 | "interface-over-type-literal": true, 15 | "member-access": false, 16 | "member-ordering": [ 17 | true, 18 | { 19 | "order": [ 20 | "static-field", 21 | "instance-field", 22 | "static-method", 23 | "instance-method" 24 | ] 25 | } 26 | ], 27 | "no-arg": true, 28 | "no-bitwise": true, 29 | "no-console": [true, "debug", "time", "timeEnd", "trace"], 30 | "no-construct": true, 31 | "no-debugger": true, 32 | "no-duplicate-super": true, 33 | "no-empty": false, 34 | "no-empty-interface": true, 35 | "no-eval": true, 36 | "no-inferrable-types": [true, "ignore-params"], 37 | "no-misused-new": true, 38 | "no-non-null-assertion": false, 39 | "no-shadowed-variable": true, 40 | "no-string-literal": false, 41 | "no-string-throw": true, 42 | "no-switch-case-fall-through": true, 43 | "no-unnecessary-initializer": true, 44 | "no-unused-expression": true, 45 | "no-var-keyword": true, 46 | "object-literal-sort-keys": false, 47 | "prefer-const": true, 48 | "radix": true, 49 | "triple-equals": [true, "allow-null-check"], 50 | "unified-signatures": true, 51 | "variable-name": false, 52 | "nx-enforce-module-boundaries": [ 53 | true, 54 | { 55 | "allow": [], 56 | "depConstraints": [ 57 | { 58 | "sourceTag": "*", 59 | "onlyDependOnLibsWithTags": ["*"] 60 | }, 61 | { 62 | "sourceTag": "common", 63 | "onlyDependOnLibsWithTags": [] 64 | }, 65 | { 66 | "sourceTag": "core", 67 | "onlyDependOnLibsWithTags": ["common"] 68 | }, 69 | { 70 | "sourceTag": "utils", 71 | "onlyDependOnLibsWithTags": ["common"] 72 | }, 73 | { 74 | "sourceTag": "app:web", 75 | "onlyDependOnLibsWithTags": ["*"] 76 | }, 77 | { 78 | "sourceTag": "app:electron", 79 | "onlyDependOnLibsWithTags": ["*"] 80 | }, 81 | { 82 | "sourceTag": "cli", 83 | "onlyDependOnLibsWithTags": ["common", "utils"] 84 | } 85 | ] 86 | } 87 | ], 88 | "directive-selector": [true, "attribute", "locl", "camelCase"], 89 | "component-selector": [true, "element", "locl", "kebab-case"], 90 | "no-conflicting-lifecycle": true, 91 | "no-host-metadata-property": true, 92 | "no-input-rename": true, 93 | "no-inputs-metadata-property": true, 94 | "no-output-native": true, 95 | "no-output-on-prefix": true, 96 | "no-output-rename": true, 97 | "no-outputs-metadata-property": true, 98 | "template-banana-in-box": true, 99 | "template-no-negated-async": true, 100 | "use-lifecycle-interface": true, 101 | "use-pipe-transform-interface": true 102 | } 103 | } 104 | --------------------------------------------------------------------------------