├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── deploy ├── deploy.sh └── deploy_rsa.enc ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── main.server.js ├── package.json ├── protractor.conf.js ├── rebuild.sh ├── script.txt ├── src ├── app │ ├── about │ │ ├── about.component.css │ │ ├── about.component.html │ │ ├── about.component.spec.ts │ │ └── about.component.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── app.server.module.ts │ ├── controls │ │ ├── plural-editor │ │ │ ├── plural-editor.component.css │ │ │ ├── plural-editor.component.html │ │ │ ├── plural-editor.component.spec.ts │ │ │ └── plural-editor.component.ts │ │ └── select-editor │ │ │ ├── select-editor.component.css │ │ │ ├── select-editor.component.html │ │ │ ├── select-editor.component.spec.ts │ │ │ └── select-editor.component.ts │ ├── forms │ │ ├── msg-edit │ │ │ ├── msg-edit.component.css │ │ │ ├── msg-edit.component.html │ │ │ ├── msg-edit.component.spec.ts │ │ │ └── msg-edit.component.ts │ │ └── new-project │ │ │ ├── new-project.component.css │ │ │ ├── new-project.component.html │ │ │ ├── new-project.component.spec.ts │ │ │ └── new-project.component.ts │ ├── frame-sidenav │ │ ├── frame-sidenav.component.css │ │ ├── frame-sidenav.component.html │ │ ├── frame-sidenav.component.spec.ts │ │ └── frame-sidenav.component.ts │ ├── frame │ │ ├── frame.component.css │ │ ├── frame.component.html │ │ ├── frame.component.spec.ts │ │ └── frame.component.ts │ ├── home │ │ ├── home.component.css │ │ ├── home.component.html │ │ ├── home.component.spec.ts │ │ └── home.component.ts │ ├── models │ │ ├── icu.ts │ │ ├── project.ts │ │ ├── source.ts │ │ ├── translation.ts │ │ ├── xlf.ts │ │ └── xmb.ts │ ├── project │ │ ├── project-resolve.ts │ │ ├── project.component.css │ │ ├── project.component.html │ │ ├── project.component.spec.ts │ │ └── project.component.ts │ ├── services │ │ ├── backend.service.spec.ts │ │ ├── backend.service.ts │ │ ├── can-deactivate-project.service.spec.ts │ │ ├── can-deactivate-project.service.ts │ │ ├── github.service.spec.ts │ │ ├── github.service.ts │ │ ├── projects.service.spec.ts │ │ └── projects.service.ts │ ├── source-msgs-list │ │ ├── source-msgs-list.component.css │ │ ├── source-msgs-list.component.html │ │ ├── source-msgs-list.component.spec.ts │ │ └── source-msgs-list.component.ts │ └── widgets │ │ ├── location-details │ │ ├── location-details.component.css │ │ ├── location-details.component.html │ │ ├── location-details.component.spec.ts │ │ └── location-details.component.ts │ │ └── progression │ │ ├── progression.component.css │ │ ├── progression.component.html │ │ ├── progression.component.spec.ts │ │ └── progression.component.ts ├── assets │ ├── .gitkeep │ └── photo.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── i18n-xlf │ ├── messages.en.xlf │ ├── messages.fr.xlf │ └── messages.xlf ├── i18n-xmb │ └── messages.xmb ├── index.html ├── main.server.ts ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app-server.json ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "angular-translator" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "testTsconfig": "tsconfig.spec.json", 20 | "prefix": "app", 21 | "styles": [ 22 | "styles.scss" 23 | ], 24 | "scripts": [], 25 | "environmentSource": "environments/environment.ts", 26 | "environments": { 27 | "dev": "environments/environment.ts", 28 | "prod": "environments/environment.prod.ts" 29 | } 30 | }, 31 | { 32 | "name": "ssr", 33 | "root": "src", 34 | "outDir": "dist/server", 35 | "assets": [ 36 | "assets", 37 | "favicon.ico" 38 | ], 39 | "main": "main.server.ts", 40 | "tsconfig": "tsconfig.app-server.json", 41 | "prefix": "app", 42 | "styles": [ 43 | "styles.scss" 44 | ], 45 | "scripts": [], 46 | "environmentSource": "environments/environment.ts", 47 | "environments": { 48 | "dev": "environments/environment.ts", 49 | "prod": "environments/environment.prod.ts" 50 | }, 51 | "platform": "server" 52 | } 53 | ], 54 | "e2e": { 55 | "protractor": { 56 | "config": "./protractor.conf.js" 57 | } 58 | }, 59 | "lint": [ 60 | { 61 | "project": "src/tsconfig.app.json" 62 | }, 63 | { 64 | "project": "src/tsconfig.spec.json" 65 | }, 66 | { 67 | "project": "e2e/tsconfig.e2e.json" 68 | } 69 | ], 70 | "test": { 71 | "karma": { 72 | "config": "./karma.conf.js" 73 | } 74 | }, 75 | "defaults": { 76 | "styleExt": "css", 77 | "component": {} 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.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 = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.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 | testem.log 34 | /typings 35 | 36 | # e2e 37 | /e2e/*.js 38 | /e2e/*.map 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | /static/ 45 | 46 | /final/ 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | env: 5 | global: 6 | - DEPLOY_USER=attack 7 | - DEPLOY_HOST=kirikou.elol.fr 8 | addons: 9 | ssh_known_hosts: kirikou.elol.fr 10 | before_script: 11 | - yarn global add @angular/cli@1.3.0-rc.3 12 | - ng version 13 | test: 14 | - npm run test 15 | before_deploy: 16 | - mkdir -p dist/en dist/fr 17 | - npm run build:all 18 | - openssl aes-256-cbc -K $encrypted_4f7d8ed15ece_key -iv $encrypted_4f7d8ed15ece_iv 19 | -in deploy/deploy_rsa.enc -out deploy/deploy_rsa -d 20 | - eval "$(ssh-agent -s)" 21 | - chmod 600 $TRAVIS_BUILD_DIR/deploy/deploy_rsa 22 | - ssh-add $TRAVIS_BUILD_DIR/deploy/deploy_rsa 23 | deploy: 24 | provider: script 25 | skip_cleanup: true 26 | script: ./deploy/deploy.sh 27 | on: 28 | branch: master 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017 Philippe MARTIN 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Translator 2 | 3 | **Angular Translator** helps you translating your Angular application. 4 | 5 | Translation applications like this one are generally not specialized for a specific framework. 6 | 7 | This application prefers to specialize in the translation of Angular applications only, so it can integrate perfectly with the framework. 8 | 9 | ## How it works 10 | The `ng-xi18n` Angular tool is responsible for extracting from the app sources the terms to translate. This tool creates a `messages.xmb` (or `messages.xlf`) file containing all the necessary information for the human translator to do his job. 11 | 12 | **Angular Translator** gets this `messages.xmb` file and uses it at the reference for the terms that have to be translated. It is called the *source file* in the app. 13 | 14 | In **Angular Translator**, you then have to declare the different file names where the different translations for specific languages will be stored. When you begin, you do not need to create these translation files, the app will do that for you. They all called the *translations files* in the app. 15 | 16 | When you have finished translating for a specific language, you can export the translation file for this language and place it beside your source file in your sources repository. If you decide to work again on the translations for this same language, the translations you have made earlier will be extracted from this translation file. 17 | 18 | When you update your app and add or remove terms to translate and regenerate the `messages.xmb` source file with `ng-xi18n`, you do not need to merge the differences to your translation files. Because **Angular Translator** gets the *source file* as the reference, it will handle correclty the terms removed or added. 19 | 20 | ## Usage 21 | 22 | First declare a new project in the application, with `New Project...` in the sidenav. You will have to declare some information for this new project: 23 | 24 | - **name**: a name that will appear in the sidenav - be clear and concise, 25 | - **icon**: a name of a Material icon - select one here: https://material.io/icons/, 26 | - **github repo**: the github repository where the app will get the files, for example `feloy/angular-translator` for translating this app, 27 | - **i18n directory**: the directory inside the repository where source and translations files are placed, `src/i18n`generally, 28 | - **source file name**: the name of the source file, `messages.xmb` or `messages.xlf`generally, 29 | - **translations files names**: the names of the translations files you want to create, one for each language you want to translate your app in - they do not need to exist in the repository. 30 | 31 | Once your project created, the app will load the contents of the *source file* and extract the terms to translate. 32 | 33 | You then have to choose one language you want to work with, based on the list of files you have declared as translations files, using the select listing the different translations files names. 34 | 35 | If the translation file exists in the repository, the app loads it and extracts the translations already done. 36 | 37 | You can now begin to translate the different terms. 38 | 39 | When you are done, you can use the `Extract` button on the upper-right corner to extract the translations in the current language in a local file. You can now commit this file in your repository. 40 | -------------------------------------------------------------------------------- /deploy/deploy.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | rsync -r --delete dist/ "${DEPLOY_USER}@${DEPLOY_HOST}:www" 4 | -------------------------------------------------------------------------------- /deploy/deploy_rsa.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feloy/angular-translator/f521781718541feb21115a6f7c2ff9b20da68094/deploy/deploy_rsa.enc -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AngularTranslatorPage } from './app.po'; 2 | 3 | describe('angular-translator App', () => { 4 | /* let page: AngularTranslatorPage; 5 | 6 | beforeEach(() => { 7 | page = new AngularTranslatorPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | });*/ 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class AngularTranslatorPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types":[ 8 | "jasmine", 9 | "node" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 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/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | files: [ 19 | { pattern: './src/test.ts', watched: false } 20 | ], 21 | preprocessors: { 22 | './src/test.ts': ['@angular/cli'] 23 | }, 24 | mime: { 25 | 'text/x-typescript': ['ts','tsx'] 26 | }, 27 | coverageIstanbulReporter: { 28 | reports: [ 'html', 'lcovonly' ], 29 | fixWebpackSourcePaths: true 30 | }, 31 | angularCli: { 32 | environment: 'dev' 33 | }, 34 | reporters: config.angularCli && config.angularCli.codeCoverage 35 | ? ['progress', 'coverage-istanbul'] 36 | : ['progress', 'kjhtml'], 37 | port: 9876, 38 | colors: true, 39 | logLevel: config.LOG_INFO, 40 | autoWatch: true, 41 | browsers: ['Chrome'], 42 | singleRun: false 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /main.server.js: -------------------------------------------------------------------------------- 1 | require('zone.js/dist/zone-node'); 2 | require('reflect-metadata'); 3 | const { renderModuleFactory } = require('@angular/platform-server'); 4 | const { enableProdMode } = require('@angular/core'); 5 | const { AppServerModuleNgFactory } = require('./dist-ssr/en/main.bundle'); 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | 9 | /* workaround */ 10 | const { MdSidenav } = require('@angular/material'); 11 | MdSidenav.prototype.ngAfterContentInit = function () {} 12 | /* /workaround */ 13 | 14 | enableProdMode(); 15 | const args = process.argv.slice(2); 16 | if (args.length != 3) { 17 | process.stdout.write("Usage: node dist/main.js \n"); 18 | process.exit(); 19 | } 20 | const indexFileContent = fs.readFileSync(args[0], 'utf8'); 21 | renderModuleFactory(AppServerModuleNgFactory, { 22 | document: indexFileContent, 23 | url: args[2] 24 | }).then(string => { 25 | let destUrl = args[2]; 26 | if (destUrl == '/') 27 | destUrl = 'index.html' 28 | const targetDir = args[1] + '/' + destUrl; 29 | targetDir.split('/').forEach((dir, index, splits) => { 30 | if (index !== splits.length - 1) { 31 | const parent = splits.slice(0, index).join('/'); 32 | const dirPath = path.resolve(parent, dir); 33 | if (!fs.existsSync(dirPath)) { 34 | fs.mkdirSync(dirPath); 35 | } 36 | } 37 | }); 38 | fs.writeFileSync(targetDir, string); 39 | console.log(targetDir); 40 | }); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-translator", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "test": "true", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e", 12 | "extract-i18n:xlf": "ng xi18n --output-path=src/i18n-xlf --locale en --i18n-format=xlf", 13 | "extract-i18n:xmb": "ng xi18n --output-path=src/i18n-xmb --locale en --i18n-format=xmb", 14 | "extract-i18n": "npm run extract-i18n:xlf && npm run extract-i18n:xmb", 15 | "build:en": "ng build --no-progress --output-path=dist/en -aot -prod --bh /en/ --i18n-file=src/i18n-xlf/messages.en.xlf --i18n-format=xlf --locale=en", 16 | "build:fr": "ng build --no-progress --output-path=dist/fr -aot -prod --bh /fr/ --i18n-file=src/i18n-xlf/messages.fr.xlf --i18n-format=xlf --locale=fr", 17 | "build:all": "npm run build:en && npm run build:fr" 18 | }, 19 | "private": true, 20 | "dependencies": { 21 | "@angular/animations": "^5.0.0", 22 | "@angular/cdk": "2.0.0-beta.8", 23 | "@angular/common": "^5.0.0", 24 | "@angular/compiler": "^5.0.0", 25 | "@angular/core": "^5.0.0", 26 | "@angular/forms": "^5.0.0", 27 | "@angular/http": "^5.0.0", 28 | "@angular/material": "2.0.0-beta.8", 29 | "@angular/platform-browser": "^5.0.0", 30 | "@angular/platform-browser-dynamic": "^5.0.0", 31 | "@angular/router": "^5.0.0", 32 | "core-js": "^2.4.1", 33 | "file-saver": "^1.3.3", 34 | "format-message-parse": "^5.1.2", 35 | "node-sass": "^4.11.0", 36 | "rxjs": "^5.4.2", 37 | "zone.js": "^0.8.14" 38 | }, 39 | "devDependencies": { 40 | "@angular/animations": "^5.0.0", 41 | "@angular/cli": "^1.7.4", 42 | "@angular/compiler-cli": "^5.0.0", 43 | "@angular/language-service": "^5.0.0", 44 | "@angular/platform-server": "^5.0.0", 45 | "@types/jasmine": "~2.5.53", 46 | "@types/node": "~6.0.60", 47 | "angular2-template-loader": "^0.6.2", 48 | "codelyzer": "~3.1.1", 49 | "imports-loader": "^0.7.1", 50 | "jasmine-core": "~2.6.2", 51 | "jasmine-spec-reporter": "~4.1.0", 52 | "karma": "~1.7.0", 53 | "karma-chrome-launcher": "~2.1.1", 54 | "karma-cli": "~1.0.1", 55 | "karma-coverage-istanbul-reporter": "^1.2.1", 56 | "karma-jasmine": "~1.1.0", 57 | "karma-jasmine-html-reporter": "^0.2.2", 58 | "protractor": "~5.1.2", 59 | "ts-node": "~3.2.0", 60 | "tslint": "~5.3.2", 61 | "typescript": "~2.4.2" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | beforeLaunch: function() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | }, 27 | onPrepare() { 28 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /rebuild.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | LANG=$1 4 | 5 | find . -name "*.ngfactory.ts" -delete 6 | find . -name "*.ngsummary.json" -delete 7 | find . -name "*.ngstyle.ts" -delete 8 | 9 | ./node_modules/.bin/ngc --i18nFile=src/i18n-xlf/messages.$LANG.xlf --i18nFormat=xlf --locale=$LANG 10 | webpack 11 | mkdir -p final/$LANG 12 | cp -R dist/$LANG/* final/$LANG 13 | node dist/main.js dist/$LANG/index.html final/$LANG / 14 | node dist/main.js dist/$LANG/index.html final/$LANG about 15 | -------------------------------------------------------------------------------- /script.txt: -------------------------------------------------------------------------------- 1 | Here is Angular Translator, the ultimate tool to translate your Angular applications. 2 | 3 | You first have to create a new project with the New Project button. 4 | 5 | There is some predefined information that you can use if you want to test the translation of this particular application. 6 | 7 | The application needs to know the github repository where your app sources can be found, 8 | the directory in which are placed the i18n files, 9 | and the names of the source and translation files. 10 | 11 | The source file contains the original terms written by the developers and the contexts for these terms (files, lines, meaning, description). 12 | 13 | The translation files will contain the translations written with this app. At this time, they are empty or even non-existant. 14 | 15 | Let's create our first project with these default information. 16 | 17 | The application has loaded the source file. It has detected its format (xmb) and has found 32 messages in it. 18 | 19 | We can now select a translation file to begin the translation for a specific language, for example french. 20 | 21 | ==== 22 | 23 | At this point, we are ready to begin the translation. 24 | 25 | Before, we can hide this information message, we can display the details of the locations where are found each terms 26 | and we can display the list of untranslated terms only. 27 | 28 | Let's start the translation. 29 | 30 | You can see in the left the term extracted from the source file. 31 | 32 | We can write our translation in the right textarea and press ctrl-enter to save and go to the next term to translate. 33 | 34 | ==== 35 | 36 | Angular uses some particular format to handle plurals and selects. We have here an example of a plural translation. 37 | 38 | The formatted language is not easy to read. 39 | 40 | We will be able to use the ICU builder to build this message with a friendly interface. 41 | 42 | At this point, we can at least understand the format of the message. -------------------------------------------------------------------------------- /src/app/about/about.component.css: -------------------------------------------------------------------------------- 1 | .main { padding: 24px; } 2 | -------------------------------------------------------------------------------- /src/app/about/about.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | Philippe MARTIN 5 | AngularAttack 2017 6 | 7 | 8 | 9 | 10 | 11 | Sources on GitHub 12 | contact@elol.fr 13 | 14 | 15 | 16 | 17 |
-------------------------------------------------------------------------------- /src/app/about/about.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AboutComponent } from './about.component'; 4 | 5 | describe('AboutComponent', () => { 6 | let component: AboutComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AboutComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AboutComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-about', 5 | templateUrl: './about.component.html', 6 | styleUrls: ['./about.component.css'] 7 | }) 8 | export class AboutComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feloy/angular-translator/f521781718541feb21115a6f7c2ff9b20da68094/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [ 9 | AppComponent 10 | ], 11 | }).compileComponents(); 12 | })); 13 | 14 | it('should create the app', async(() => { 15 | const fixture = TestBed.createComponent(AppComponent); 16 | const app = fixture.debugElement.componentInstance; 17 | expect(app).toBeTruthy(); 18 | })); 19 | 20 | it(`should have as title 'app works!'`, async(() => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.debugElement.componentInstance; 23 | expect(app.title).toEqual('app works!'); 24 | })); 25 | 26 | it('should render title in a h1 tag', async(() => { 27 | const fixture = TestBed.createComponent(AppComponent); 28 | fixture.detectChanges(); 29 | const compiled = fixture.debugElement.nativeElement; 30 | expect(compiled.querySelector('h1').textContent).toContain('app works!'); 31 | })); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'app works!'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { CanDeactivateProjectService } from './services/can-deactivate-project.service'; 2 | import { GithubService } from './services/github.service'; 3 | import { ProjectResolve } from './project/project-resolve'; 4 | import { BackendService } from './services/backend.service'; 5 | import { ProjectsService } from './services/projects.service'; 6 | import { BrowserModule } from '@angular/platform-browser'; 7 | import { NgModule } from '@angular/core'; 8 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 9 | import { HttpModule } from '@angular/http'; 10 | import { RouterModule } from '@angular/router'; 11 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 12 | 13 | import { MaterialModule } from '@angular/material'; 14 | 15 | import { AppComponent } from './app.component'; 16 | import { FrameComponent } from './frame/frame.component'; 17 | import { FrameSidenavComponent } from './frame-sidenav/frame-sidenav.component'; 18 | import { NewProjectComponent } from './forms/new-project/new-project.component'; 19 | import { ProjectComponent } from './project/project.component'; 20 | import { ProgressionComponent } from './widgets/progression/progression.component'; 21 | import { SourceMsgsListComponent } from './source-msgs-list/source-msgs-list.component'; 22 | import { MsgEditComponent } from './forms/msg-edit/msg-edit.component'; 23 | import { HomeComponent } from './home/home.component'; 24 | import { PluralEditorComponent } from './controls/plural-editor/plural-editor.component'; 25 | import { SelectEditorComponent } from './controls/select-editor/select-editor.component'; 26 | import { LocationDetailsComponent } from './widgets/location-details/location-details.component'; 27 | import { AboutComponent } from './about/about.component'; 28 | 29 | @NgModule({ 30 | declarations: [ 31 | AppComponent, 32 | FrameComponent, 33 | FrameSidenavComponent, 34 | NewProjectComponent, 35 | ProjectComponent, 36 | ProgressionComponent, 37 | SourceMsgsListComponent, 38 | MsgEditComponent, 39 | HomeComponent, 40 | PluralEditorComponent, 41 | SelectEditorComponent, 42 | LocationDetailsComponent, 43 | AboutComponent, 44 | ], 45 | imports: [ 46 | BrowserModule.withServerTransition({ 47 | appId: 'angular-translator' 48 | }), 49 | ReactiveFormsModule, 50 | FormsModule, 51 | HttpModule, 52 | RouterModule.forRoot([ 53 | { 54 | path: '', component: FrameComponent, 55 | children: [ 56 | { path: '', component: HomeComponent }, 57 | { path: 'about', component: AboutComponent }, 58 | { path: 'newproject', component: NewProjectComponent }, 59 | { 60 | path: 'project/:id/edit', component: NewProjectComponent, resolve: { project: ProjectResolve } 61 | }, 62 | { 63 | path: 'project/:id', component: ProjectComponent, 64 | resolve: { project: ProjectResolve }, 65 | canDeactivate: [ CanDeactivateProjectService ], 66 | children: [ 67 | { path: ':msgid', component: MsgEditComponent } 68 | ] 69 | } 70 | ] 71 | } 72 | ]), 73 | NoopAnimationsModule, 74 | MaterialModule 75 | ], 76 | providers: [ 77 | BackendService, 78 | GithubService, 79 | ProjectsService, 80 | ProjectResolve, 81 | CanDeactivateProjectService 82 | ], 83 | 84 | bootstrap: [AppComponent] 85 | }) 86 | export class AppModule { } 87 | -------------------------------------------------------------------------------- /src/app/app.server.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ServerModule } from '@angular/platform-server'; 3 | import { AppComponent } from './app.component'; 4 | import { AppModule } from './app.module'; 5 | @NgModule({ 6 | imports: [ 7 | AppModule, 8 | ServerModule 9 | ], 10 | bootstrap: [ 11 | AppComponent 12 | ] 13 | }) 14 | export class AppServerModule {} 15 | -------------------------------------------------------------------------------- /src/app/controls/plural-editor/plural-editor.component.css: -------------------------------------------------------------------------------- 1 | .main { margin: 8px; padding: 4px; border: 1px solid #888; } -------------------------------------------------------------------------------- /src/app/controls/plural-editor/plural-editor.component.html: -------------------------------------------------------------------------------- 1 |
plural 2 |
3 | 4 |
{{case.key}}
5 |
{{case.value}}
6 |
7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /src/app/controls/plural-editor/plural-editor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PluralEditorComponent } from './plural-editor.component'; 4 | 5 | describe('PluralEditorComponent', () => { 6 | let component: PluralEditorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ PluralEditorComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PluralEditorComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/controls/plural-editor/plural-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { Msg, FormatMessage, FormatMessageCase } from './../../models/source'; 2 | import { Component, OnInit, Input } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'app-plural-editor', 6 | templateUrl: './plural-editor.component.html', 7 | styleUrls: ['./plural-editor.component.css'] 8 | }) 9 | export class PluralEditorComponent implements OnInit { 10 | 11 | @Input() content: FormatMessage; 12 | constructor() { } 13 | 14 | ngOnInit() { 15 | } 16 | 17 | public isIcu(v: string | FormatMessageCase[]): boolean { 18 | return v instanceof Object; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/controls/select-editor/select-editor.component.css: -------------------------------------------------------------------------------- 1 | .main { margin: 8px; padding: 4px; border: 1px solid #888; } -------------------------------------------------------------------------------- /src/app/controls/select-editor/select-editor.component.html: -------------------------------------------------------------------------------- 1 |
select 2 |
3 | 4 |
{{case.key}}
5 |
{{case.value}}
6 |
7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /src/app/controls/select-editor/select-editor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SelectEditorComponent } from './select-editor.component'; 4 | 5 | describe('SelectEditorComponent', () => { 6 | let component: SelectEditorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SelectEditorComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SelectEditorComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/controls/select-editor/select-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { Msg, FormatMessage, FormatMessageCase } from './../../models/source'; 2 | import { Component, OnInit, Input } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'app-select-editor', 6 | templateUrl: './select-editor.component.html', 7 | styleUrls: ['./select-editor.component.css'] 8 | }) 9 | export class SelectEditorComponent implements OnInit { 10 | 11 | @Input() content: FormatMessage; 12 | 13 | constructor() { } 14 | 15 | ngOnInit() { 16 | } 17 | 18 | public isIcu(v: string | FormatMessageCase[]): boolean { 19 | return v instanceof Object; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/forms/msg-edit/msg-edit.component.css: -------------------------------------------------------------------------------- 1 | md-card { margin: 0;} 2 | 3 | .taflex { display: flex; } 4 | .taflex > textarea { margin: 8px; padding: 4px; flex-grow: 1; font-size: 20px; } 5 | 6 | .buttons { text-align: right; } 7 | .infos { margin: 0 16px; } 8 | .locations { margin: 16px 0; } 9 | .locations > div > .title { font-weight: bold; } 10 | .locations > .location { margin: 8px 0; } 11 | -------------------------------------------------------------------------------- /src/app/forms/msg-edit/msg-edit.component.html: -------------------------------------------------------------------------------- 1 | 2 | Press ctrl-enter to save & go to next untranslated message 3 | 4 | 5 |
6 | ICU Builder (prototype) 7 |
8 | 9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 | 24 |
25 |
meaning: {{srcMsg.meaning}}
26 |
description: {{srcMsg.desc}}
27 | 28 |
29 |
30 | Locations: 31 | view details 32 |
33 | 34 |
{{location.sourcefile}}:{{location.linenumber}}
35 |
36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 | 44 | 45 | 46 | 47 |
-------------------------------------------------------------------------------- /src/app/forms/msg-edit/msg-edit.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MsgEditComponent } from './msg-edit.component'; 4 | 5 | describe('MsgEditComponent', () => { 6 | let component: MsgEditComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ MsgEditComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MsgEditComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/forms/msg-edit/msg-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { Msg, Source } from './../../models/source'; 2 | import { ProjectsService } from './../../services/projects.service'; 3 | import { Translation } from './../../models/translation'; 4 | import { ActivatedRoute } from '@angular/router'; 5 | import { Component, OnInit } from '@angular/core'; 6 | import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; 7 | import { Observable } from 'rxjs/Observable'; 8 | import 'rxjs/add/observable/combineLatest'; 9 | 10 | @Component({ 11 | selector: 'app-msg-edit', 12 | templateUrl: './msg-edit.component.html', 13 | styleUrls: ['./msg-edit.component.css'] 14 | }) 15 | export class MsgEditComponent implements OnInit { 16 | 17 | public icuBuilder = false; 18 | public locationsDetails = false; 19 | 20 | public trMsg: Msg = null; 21 | public srcMsg: Msg = null; 22 | 23 | public projectId: number; 24 | private msgId: string; 25 | 26 | form: FormGroup; 27 | srcCtrl: FormControl; 28 | trCtrl: FormControl; 29 | 30 | constructor(private fb: FormBuilder, private route: ActivatedRoute, private projects: ProjectsService) { } 31 | 32 | ngOnInit() { 33 | 34 | this.createForm(); 35 | 36 | const msgId$ = this.route.params.pluck('msgid'); 37 | const projectId$ = this.route.parent.params.pluck('id'); 38 | const tr$ = this.projects.currentTranslation$; 39 | const src$ = this.projects.currentSource$; 40 | 41 | Observable.combineLatest(projectId$, msgId$, tr$, src$) 42 | .subscribe(([projectId, msgId, tr, src]: [string, string, Translation, Source]) => { 43 | if (projectId === null || msgId === null || tr === null || src === null) { 44 | this.trMsg = null; 45 | this.srcMsg = null; 46 | } else { 47 | this.projectId = +projectId; 48 | this.msgId = msgId; 49 | 50 | const trList = tr.msgs.filter((m: Msg) => m.id === msgId); 51 | this.trMsg = trList.length > 0 ? trList[0] : { id: msgId, content: '', icu: null }; 52 | 53 | const srcList = src.msgs.filter((m: Msg) => m.id === msgId); 54 | this.srcMsg = srcList.length > 0 ? srcList[0] : { id: msgId, content: '', icu: null }; 55 | 56 | this.createForm(); 57 | this.form.setValue({ 58 | src: this.srcMsg.content, 59 | tr: this.trMsg.content 60 | }); 61 | } 62 | }); 63 | } 64 | 65 | private createForm() { 66 | this.srcCtrl = new FormControl(''); 67 | this.trCtrl = new FormControl(''); 68 | this.form = this.fb.group({ 69 | src: this.srcCtrl, 70 | tr: this.trCtrl 71 | }); 72 | } 73 | 74 | public onControlEnter() { 75 | if (this.form.get('tr').value) { 76 | this.onSave(); 77 | } else { 78 | this.form.get('tr').setValue(this.form.get('src').value); 79 | } 80 | } 81 | 82 | public onSave() { 83 | this.projects.setTranslatedMsg(this.msgId, this.form.get('tr').value); 84 | } 85 | 86 | public onCancel() { 87 | this.createForm(); 88 | this.form.setValue({ 89 | src: this.srcMsg.content, 90 | tr: this.trMsg.content 91 | }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/app/forms/new-project/new-project.component.css: -------------------------------------------------------------------------------- 1 | .main { padding: 16px; } 2 | md-input-container { width: 100%; } 3 | .buttons { text-align: right; } 4 | -------------------------------------------------------------------------------- /src/app/forms/new-project/new-project.component.html: -------------------------------------------------------------------------------- 1 | Create new project 2 | {{project.name}} edition 3 | 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 |
30 |
31 |
-------------------------------------------------------------------------------- /src/app/forms/new-project/new-project.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NewProjectComponent } from './new-project.component'; 4 | 5 | describe('NewProjectComponent', () => { 6 | let component: NewProjectComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NewProjectComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NewProjectComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/forms/new-project/new-project.component.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from './../../models/translation'; 2 | import { Project } from './../../models/project'; 3 | import { ProjectsService } from './../../services/projects.service'; 4 | import { Component, OnInit } from '@angular/core'; 5 | import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; 6 | import { Router, ActivatedRoute } from '@angular/router'; 7 | import 'rxjs/add/operator/pluck'; 8 | 9 | @Component({ 10 | selector: 'app-new-project', 11 | templateUrl: './new-project.component.html', 12 | styleUrls: ['./new-project.component.css'] 13 | }) 14 | export class NewProjectComponent implements OnInit { 15 | 16 | public project: Project; 17 | 18 | form: FormGroup; 19 | iconCtrl: FormControl; 20 | nameCtrl: FormControl; 21 | repoCtrl: FormControl; 22 | i18ndirCtrl: FormControl; 23 | sourceCtrl: FormControl; 24 | translationsCtrl: FormControl; 25 | 26 | defaultIcon = 'favorite'; 27 | defaultName = 'translator'; 28 | defaultRepo = 'feloy/angular-translator'; 29 | defaultI18ndir = 'src/i18n-xmb'; 30 | defaultSource = 'messages.xmb'; 31 | defaultTranslations = `messages.fr.xmb 32 | messages.en.xmb`; 33 | 34 | constructor(private fb: FormBuilder, private router: Router, 35 | private route: ActivatedRoute, private projects: ProjectsService) { } 36 | 37 | ngOnInit() { 38 | this.route.data.pluck('project').subscribe((p: Project) => { 39 | this.project = p; 40 | this.createForm(p); 41 | }); 42 | } 43 | 44 | onAdd() { 45 | const id = this.projects.add({ 46 | icon: this.iconCtrl.value, 47 | name: this.nameCtrl.value, 48 | repo: this.repoCtrl.value, 49 | i18ndir: this.i18ndirCtrl.value, 50 | source: this.sourceCtrl.value, 51 | translations: this.translationsCtrl.value.split('\n') 52 | }); 53 | this.router.navigate(['/project', id]); 54 | } 55 | 56 | onSave() { 57 | console.log(this.translationsCtrl.value); 58 | this.projects.update(this.project.id, { 59 | icon: this.iconCtrl.value, 60 | name: this.nameCtrl.value, 61 | repo: this.repoCtrl.value, 62 | i18ndir: this.i18ndirCtrl.value, 63 | source: this.sourceCtrl.value, 64 | translations: this.translationsCtrl.value.split('\n') 65 | }); 66 | this.router.navigate(['/project', this.project.id]); 67 | } 68 | 69 | onCancel() { 70 | if (this.project) { 71 | this.router.navigate(['/project', this.project.id]); 72 | } else { 73 | this.router.navigate(['/']); 74 | } 75 | } 76 | 77 | onDelete() { 78 | this.projects.delete(this.project.id); 79 | this.router.navigate(['/']); 80 | } 81 | 82 | private createForm(p: Project) { 83 | this.iconCtrl = new FormControl(p ? p.icon : this.defaultIcon, Validators.required); 84 | this.nameCtrl = new FormControl(p ? p.name : this.defaultName, Validators.required); 85 | this.repoCtrl = new FormControl(p ? p.repo : this.defaultRepo, Validators.required); 86 | this.i18ndirCtrl = new FormControl(p ? p.i18ndir : this.defaultI18ndir, Validators.required); 87 | this.sourceCtrl = new FormControl(p ? p.source : this.defaultSource, Validators.required); 88 | this.translationsCtrl = new FormControl(p ? p.translations.join('\n') : this.defaultTranslations, Validators.required); 89 | this.form = this.fb.group({ 90 | icon: this.iconCtrl, 91 | name: this.nameCtrl, 92 | repo: this.repoCtrl, 93 | i18ndir: this.i18ndirCtrl, 94 | source: this.sourceCtrl, 95 | translations: this.translationsCtrl 96 | }); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/app/frame-sidenav/frame-sidenav.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feloy/angular-translator/f521781718541feb21115a6f7c2ff9b20da68094/src/app/frame-sidenav/frame-sidenav.component.css -------------------------------------------------------------------------------- /src/app/frame-sidenav/frame-sidenav.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | home 5 | Home 6 | 7 | 8 | 9 | {{project.icon}} 10 | {{project.name}} 11 | 12 | 13 | 14 | add 15 | New project... 16 | 17 | 18 | 19 | tag_faces 20 | About 21 | 22 | 23 |

Interface Language

24 | 25 | 26 | {{lang.label}} 27 | {{lang.label}} 28 | 29 |
30 | -------------------------------------------------------------------------------- /src/app/frame-sidenav/frame-sidenav.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FrameSidenavComponent } from './frame-sidenav.component'; 4 | 5 | describe('FrameSidenavComponent', () => { 6 | let component: FrameSidenavComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ FrameSidenavComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FrameSidenavComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/frame-sidenav/frame-sidenav.component.ts: -------------------------------------------------------------------------------- 1 | import { Project } from './../models/project'; 2 | import { ProjectsService } from './../services/projects.service'; 3 | import { Component, OnInit, LOCALE_ID, Inject } from '@angular/core'; 4 | import { Observable } from 'rxjs/Observable'; 5 | 6 | @Component({ 7 | selector: 'app-frame-sidenav', 8 | templateUrl: './frame-sidenav.component.html', 9 | styleUrls: ['./frame-sidenav.component.css'] 10 | }) 11 | export class FrameSidenavComponent implements OnInit { 12 | 13 | public languages = [ 14 | { code: 'en', label: 'English' }, 15 | { code: 'fr', label: 'Français' } 16 | ]; 17 | 18 | public projects: Observable; 19 | 20 | constructor( @Inject(LOCALE_ID) protected localeId: string, 21 | private projectsService: ProjectsService) { } 22 | 23 | ngOnInit() { 24 | this.projects = this.projectsService.projects$; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/frame/frame.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feloy/angular-translator/f521781718541feb21115a6f7c2ff9b20da68094/src/app/frame/frame.component.css -------------------------------------------------------------------------------- /src/app/frame/frame.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 |
Angular Translator
7 |
8 | 9 | 10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /src/app/frame/frame.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FrameComponent } from './frame.component'; 4 | 5 | describe('FrameComponent', () => { 6 | let component: FrameComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ FrameComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FrameComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/frame/frame.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy, ViewChild, PLATFORM_ID } from '@angular/core'; 2 | import { isPlatformBrowser } from '@angular/common'; 3 | import { Subscription } from 'rxjs/Subscription'; 4 | import { Router } from '@angular/router'; 5 | import { MdSidenav } from '@angular/material'; 6 | 7 | @Component({ 8 | selector: 'app-frame', 9 | templateUrl: './frame.component.html', 10 | styleUrls: ['./frame.component.css'] 11 | }) 12 | export class FrameComponent implements OnInit { 13 | 14 | @ViewChild(MdSidenav) sidenav: MdSidenav; 15 | 16 | constructor(private router: Router) { } 17 | 18 | ngOnInit() { } 19 | 20 | public onSidenavClicked() { 21 | this.sidenav.close(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/app/home/home.component.css: -------------------------------------------------------------------------------- 1 | .main { padding: 16px; } -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Angular Translator helps you translating your Angular application.

3 | 4 |

Translation applications like this one are generally not specialized for a specific framework.

5 | 6 |

This application prefers to specialize in the translation of Angular applications only, so it can integrate perfectly with 7 | the framework.

8 | 9 |

How it works

10 | 11 |

The ng-xi18n Angular tool is responsible for extracting from the app sources the terms to translate. This tool creates a messages.xmb (or messages.xlf) file containing all the necessary information for the human translator to do his job.

12 | 13 |

Angular Translator gets this messages.xmb file and uses it at the reference for the terms that have to be translated. It is called the source file in the app.

14 | 15 |

In Angular Translator, you then have to declare the different file names where the different translations for specific languages will be stored. When you begin, you do not need to create these translation files, the app will do that for you. They all called the translations files in the app.

16 | 17 |

When you have finished translating for a specific language, you can export the translation file for this language and place it beside your source file in your sources repository. If you decide to work again on the translations for this same language, the translations you have made earlier will be extracted from this translation file.

18 | 19 |

When you update your app and add or remove terms to translate and regenerate the messages.xmb source file with ng-xi18n, you do not need to merge the differences to your translation files. Because Angular Translator gets the source file as the reference, it will handle correclty the terms removed or added.

20 | 21 |

Usage

22 | 23 |

First declare a new project in the application, with New Project... in the sidenav. You will have to declare some information for this new project:

24 | 25 |
    26 |
  • name: a name that will appear in the sidenav - be clear and concise, 27 |
  • icon: a name of a Material icon - select one here: https://material.io/icons/, 28 |
  • github repo: the github repository where the app will get the files, for example feloy/angular-translator for translating this app, 29 |
  • i18n directory: the directory inside the repository where source and translations files are placed, src/i18ngenerally, 30 |
  • source file name: the name of the source file, messages.xmb or messages.xlfgenerally, 31 |
  • translations files names: the names of the translations files you want to create, one for each language you want to translate your app in - they do not need to exist in the repository. 32 |
33 | 34 | Once your project created, the app will load the contents of the source file and extract the terms to translate. 35 | 36 | You then have to choose one language you want to work with, based on the list of files you have declared as translations files, using the select listing the different translations files names. 37 | 38 | If the translation file exists in the repository, the app loads it and extracts the translations already done. 39 | 40 | You can now begin to translate the different terms. 41 | 42 | When you are done, you can use the Extract button on the upper-right corner to extract the translations in the current language in a local file. You can now commit this file in your repository. 43 |
44 | -------------------------------------------------------------------------------- /src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HomeComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home', 5 | templateUrl: './home.component.html', 6 | styleUrls: ['./home.component.css'] 7 | }) 8 | export class HomeComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/models/icu.ts: -------------------------------------------------------------------------------- 1 | import { FormatMessage, FormatMessageCase } from './source'; 2 | import parse from 'format-message-parse'; 3 | 4 | export class Icu { 5 | 6 | static parse(str: string): FormatMessage { 7 | const res = parse(str); 8 | return this.subparse(res, true); 9 | } 10 | 11 | static subparse(res: any, returnNullIfString: boolean) { 12 | if (res instanceof Array && res.length === 1 && typeof res[0] === 'string') { 13 | return returnNullIfString ? null : res[0]; 14 | } else if (res instanceof Array && res.length === 1 && res[0] instanceof Array) { 15 | 16 | let ret: string | FormatMessage = null; 17 | const cases: FormatMessageCase[] = []; 18 | let rawCases; 19 | 20 | switch (res[0][1]) { 21 | case 'select': 22 | rawCases = res[0][2]; 23 | for (const key in rawCases) { 24 | if (rawCases.hasOwnProperty(key)) { 25 | cases.push({ key: key, value: this.subparse(rawCases[key], false) }); 26 | } 27 | } 28 | ret = { 29 | type: 'select', 30 | value: cases 31 | }; 32 | break; 33 | 34 | case 'plural': 35 | rawCases = res[0][3]; 36 | for (const key in rawCases) { 37 | if (rawCases.hasOwnProperty(key)) { 38 | cases.push({ key: key, value: this.subparse(rawCases[key], false) }); 39 | } 40 | } 41 | ret = { 42 | type: 'plural', 43 | value: cases 44 | }; 45 | break; 46 | 47 | default: 48 | ret = res.join(' '); 49 | } 50 | return ret; 51 | } else { 52 | return res.join(''); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/models/project.ts: -------------------------------------------------------------------------------- 1 | export interface Project { 2 | id?: number; 3 | icon: string; 4 | name: string; 5 | repo: string; 6 | i18ndir: string; 7 | source: string; 8 | translations: string[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/models/source.ts: -------------------------------------------------------------------------------- 1 | export interface Source { 2 | msgs: Msg[]; 3 | } 4 | 5 | export interface FormatMessageCase { 6 | key: string; 7 | value: string | FormatMessage; 8 | } 9 | 10 | export interface FormatMessage { 11 | type: 'select' | 'plural'; 12 | value: string | FormatMessageCase[]; 13 | } 14 | 15 | export interface Msg { 16 | id: string; 17 | desc?: string; 18 | meaning?: string; 19 | locations?: Location[]; 20 | content: string; 21 | icu: FormatMessage; 22 | } 23 | 24 | export interface Location { 25 | sourcefile: string; 26 | linenumber: number; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/models/translation.ts: -------------------------------------------------------------------------------- 1 | import { Msg } from './source'; 2 | export interface Translation { 3 | msgs: Msg[]; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/models/xlf.ts: -------------------------------------------------------------------------------- 1 | import { Icu } from './icu'; 2 | import { Translation } from './translation'; 3 | import { Source, Msg } from './source'; 4 | 5 | export class Xlf { 6 | 7 | static parseSource(data: string): Source { 8 | try { 9 | const source = { 10 | msgs: [] 11 | }; 12 | const parser = new DOMParser(); 13 | const xmlDoc = parser.parseFromString(data, 'text/xml'); 14 | const domMsgs = xmlDoc.getElementsByTagName('trans-unit'); 15 | for (let i = 0; i < domMsgs.length; i++) { 16 | const domMsg = domMsgs.item(i); 17 | const msg: Msg = { 18 | id: domMsg.getAttribute('id'), 19 | locations: [], 20 | content: domMsg.getElementsByTagName('source')[0].innerHTML, 21 | icu: Icu.parse(domMsg.getElementsByTagName('source')[0].innerHTML) 22 | }; 23 | 24 | const notes = domMsg.getElementsByTagName('note'); 25 | for (let j = 0; j < notes.length; j++) { 26 | const note = notes.item(j); 27 | const from = note.getAttribute('from'); 28 | if (from === 'description') { 29 | msg.desc = note.textContent; 30 | } else if (from === 'meaning') { 31 | msg.meaning = note.textContent; 32 | } 33 | } 34 | 35 | const contextGroups = domMsg.getElementsByTagName('context-group'); 36 | for (let j = 0; j < contextGroups.length; j++) { 37 | const contextGroup = contextGroups.item(j); 38 | const purpose = contextGroup.getAttribute('purpose'); 39 | if (purpose !== 'location') { 40 | continue; 41 | } 42 | const contexts = contextGroup.getElementsByTagName('context'); 43 | let sourcefile = '', linenumber = -1; 44 | for (let k = 0; k < contexts.length; k++) { 45 | const context = contexts.item(k); 46 | const type = context.getAttribute('context-type'); 47 | switch (type) { 48 | case 'sourcefile': sourcefile = context.textContent; break; 49 | case 'linenumber': linenumber = parseInt(context.textContent, 10); break; 50 | } 51 | } 52 | msg.locations.push({ sourcefile, linenumber }); 53 | } 54 | 55 | source.msgs.push(msg); 56 | } 57 | return source; 58 | } catch (e) { 59 | console.log(e); 60 | return null; 61 | } 62 | } 63 | 64 | static parseTranslation(data: string): Translation { 65 | try { 66 | const translation = { 67 | msgs: [] 68 | }; 69 | const parser = new DOMParser(); 70 | const xmlDoc = parser.parseFromString(data, 'text/xml'); 71 | const domMsgs = xmlDoc.getElementsByTagName('trans-unit'); 72 | for (let i = 0; i < domMsgs.length; i++) { 73 | const domMsg = domMsgs.item(i); 74 | const msg: Msg = { 75 | id: domMsg.getAttribute('id'), 76 | content: domMsg.getElementsByTagName('target')[0].innerHTML, 77 | icu: Icu.parse(domMsg.getElementsByTagName('target')[0].innerHTML) 78 | }; 79 | 80 | translation.msgs.push(msg); 81 | } 82 | return translation; 83 | } catch (e) { 84 | console.log(e); 85 | return null; 86 | } 87 | } 88 | 89 | static export(src: Source, tr: Translation): string { 90 | 91 | let xlf = ` 92 | 93 | 94 | `; 95 | 96 | src.msgs.map((s: Msg) => { 97 | let target = ''; 98 | const list = tr.msgs.filter((m: Msg) => m.id === s.id); 99 | if (list.length > 0) { 100 | target = list[0].content; 101 | } 102 | xlf += ` 103 | 104 | ` + s.content + ` 105 | ` + target + ` 106 | `; 107 | }); 108 | 109 | xlf += ` 110 | 111 | `; 112 | return xlf; 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/app/models/xmb.ts: -------------------------------------------------------------------------------- 1 | import { Icu } from './icu'; 2 | import { Translation } from './translation'; 3 | import { Source, Msg } from './source'; 4 | 5 | export class Xmb { 6 | 7 | static parseSource(data: string): Source { 8 | try { 9 | const source = { 10 | msgs: [] 11 | }; 12 | const parser = new DOMParser(); 13 | const xmlDoc = parser.parseFromString(data, 'text/xml'); 14 | const domMsgs = xmlDoc.getElementsByTagName('msg'); 15 | for (let i = 0; i < domMsgs.length; i++) { 16 | const domMsg = domMsgs.item(i); 17 | const msg: Msg = { 18 | id: domMsg.getAttribute('id'), 19 | desc: domMsg.getAttribute('desc'), 20 | meaning: domMsg.getAttribute('meaning'), 21 | locations: [], 22 | content: '', 23 | icu: null 24 | }; 25 | const domLocations = domMsg.getElementsByTagName('source'); 26 | for (let j = domLocations.length - 1; j >= 0; j--) { 27 | const domLocation = domLocations.item(j); 28 | const [sourcefile, linenumber] = domLocation.textContent.split(':'); 29 | msg.locations.push({ sourcefile, linenumber: parseInt(linenumber, 10) }); 30 | domMsg.removeChild(domLocation); 31 | } 32 | msg.content = domMsg.innerHTML; 33 | msg.icu = Icu.parse(domMsg.innerHTML); 34 | source.msgs.push(msg); 35 | } 36 | 37 | return source; 38 | } catch (e) { 39 | return null; 40 | } 41 | } 42 | 43 | static parseTranslation(data: string): Translation { 44 | try { 45 | const translation = { 46 | msgs: [] 47 | }; 48 | const parser = new DOMParser(); 49 | const xmlDoc = parser.parseFromString(data, 'text/xml'); 50 | const domMsgs = xmlDoc.getElementsByTagName('msg'); 51 | for (let i = 0; i < domMsgs.length; i++) { 52 | const domMsg = domMsgs.item(i); 53 | const msg: Msg = { 54 | id: domMsg.getAttribute('id'), 55 | content: '', 56 | icu: null 57 | }; 58 | const domLocations = domMsg.getElementsByTagName('source'); 59 | for (let j = domLocations.length - 1; j >= 0; j--) { 60 | const domLocation = domLocations.item(j); 61 | domMsg.removeChild(domLocation); 62 | } 63 | msg.content = domMsg.innerHTML; 64 | msg.icu = Icu.parse(domMsg.innerHTML); 65 | translation.msgs.push(msg); 66 | } 67 | 68 | return translation; 69 | } catch (e) { 70 | return null; 71 | } 72 | } 73 | 74 | static export(src: Source, tr: Translation): string { 75 | let xmb = ` 76 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | ]> 97 | `; 98 | 99 | src.msgs.map((s: Msg) => { 100 | let target = ''; 101 | const list = tr.msgs.filter((m: Msg) => m.id === s.id); 102 | if (list.length > 0) { 103 | target = list[0].content; 104 | } 105 | xmb += ` 106 | ` + target + ``; 107 | }); 108 | 109 | xmb += ` 110 | `; 111 | return xmb; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/app/project/project-resolve.ts: -------------------------------------------------------------------------------- 1 | import { ProjectsService } from './../services/projects.service'; 2 | import { Project } from './../models/project'; 3 | import { Injectable } from '@angular/core'; 4 | import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 5 | import { Observable } from 'rxjs/Observable'; 6 | 7 | @Injectable() 8 | export class ProjectResolve implements Resolve { 9 | 10 | constructor(private projects: ProjectsService) { } 11 | 12 | resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Project { 13 | const id = +route.params['id']; 14 | return this.projects.get(id); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/project/project.component.css: -------------------------------------------------------------------------------- 1 | .main { padding: 16px; } 2 | h2 { margin: 0 0 8px 0; } 3 | .info { font-size: small; } 4 | .select.right { text-align: right; } 5 | 6 | .header { display: flex; padding-bottom: 16px; } 7 | .info { flex-grow: 1; } 8 | .select { flex-grow: 0; } 9 | 10 | .untrans { margin-top: 16px; } 11 | 12 | .pointer { cursor: pointer; } -------------------------------------------------------------------------------- /src/app/project/project.component.html: -------------------------------------------------------------------------------- 1 | {{project.name}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 |
18 |
{{project.repo}}
19 | {source.msgs.length, plural, =0 {no message} =1 {1 message} other {{{source.msgs.length}} messages}} 20 | , {countTranslated, plural, =0 {0 translated} =1 {1 translated} other {{{countTranslated}} translated}} 21 | * 22 |
23 | 24 |
25 | Select a translation: 26 | 27 | {{translation}} 28 | 29 |
30 |
31 | 32 | 33 | 34 | Untranslated only 35 | 36 | 38 |
-------------------------------------------------------------------------------- /src/app/project/project.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProjectComponent } from './project.component'; 4 | 5 | describe('ProjectComponent', () => { 6 | let component: ProjectComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProjectComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProjectComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/project/project.component.ts: -------------------------------------------------------------------------------- 1 | import { CanComponentDeactivate } from './../services/can-deactivate-project.service'; 2 | import { Xlf } from './../models/xlf'; 3 | import { Xmb } from './../models/xmb'; 4 | import { ProjectsService } from './../services/projects.service'; 5 | import { Translation } from './../models/translation'; 6 | import { Source, Msg } from './../models/source'; 7 | import { GithubService } from './../services/github.service'; 8 | import { Project } from './../models/project'; 9 | import { Component, OnInit, OnDestroy } from '@angular/core'; 10 | import { MdSnackBar } from '@angular/material'; 11 | 12 | import { ActivatedRoute, Router } from '@angular/router'; 13 | import { Subscription } from 'rxjs/Subscription'; 14 | 15 | import { Observable } from 'rxjs/Observable'; 16 | import 'rxjs/add/operator/pluck'; 17 | import 'rxjs/add/operator/do'; 18 | import 'rxjs/add/operator/map'; 19 | import 'rxjs/add/observable/merge'; 20 | import * as FileSaver from 'file-saver'; 21 | 22 | @Component({ 23 | selector: 'app-project', 24 | templateUrl: './project.component.html', 25 | styleUrls: ['./project.component.css'] 26 | }) 27 | export class ProjectComponent implements OnInit, OnDestroy, CanComponentDeactivate { 28 | 29 | public project: Project; 30 | private subs: Subscription[] = []; 31 | public progression: Observable = null; 32 | public source: Source = null; 33 | public translation: Translation = null; 34 | private filename = 'output.xml'; 35 | public untranslatedOnlyToggle: boolean; 36 | public countTranslated; 37 | public needSave: boolean; 38 | 39 | constructor(private route: ActivatedRoute, private router: Router, private github: GithubService, 40 | private projects: ProjectsService, private snackBar: MdSnackBar) { } 41 | 42 | ngOnInit() { 43 | this.subs.push(this.route.data.pluck('project').subscribe((p: Project) => { 44 | this.project = p; 45 | this.progression = null; 46 | this.setSource(null); 47 | this.onReload(); 48 | })); 49 | 50 | this.subs.push(this.projects.currentTranslation$.subscribe((tr: Translation) => { 51 | this.translation = tr; 52 | 53 | this.countTranslated = this.translation ? this.translation.msgs.filter((m: Msg) => m.content !== '').length : 0; 54 | 55 | // Go to first untranslated message 56 | if (this.source && this.translation) { 57 | const list = this.source.msgs.filter((m: Msg) => { 58 | return this.translation.msgs.filter((t: Msg) => t.id === m.id && t.content !== '').length === 0; 59 | }); 60 | if (list.length > 0) { 61 | this.navigateTo(list[0].id); 62 | } 63 | } 64 | })); 65 | 66 | this.subs.push(this.projects.needSave$.subscribe((b: boolean) => this.needSave = b)); 67 | } 68 | 69 | public onReload() { 70 | this.setSource(null); 71 | this.progression = null; 72 | this.setTranslation(null); 73 | setTimeout(() => { 74 | this.progression = this.github.getSource(this.project) 75 | .do(v => { 76 | if (typeof v !== 'string' && typeof v !== 'boolean') { 77 | this.setSource(v); 78 | } 79 | }); 80 | }, 0); 81 | } 82 | 83 | public onTranslationChanged(translation: string) { 84 | this.filename = translation; 85 | this.progression = null; 86 | this.setTranslation(null); 87 | setTimeout(() => { 88 | this.progression = this.github.getTranslation(this.project, translation) 89 | .do(v => { 90 | if (typeof v !== 'string' && typeof v !== 'boolean') { 91 | this.setTranslation(v); 92 | } 93 | }); 94 | }, 0); 95 | } 96 | 97 | private navigateTo(msgId: string) { 98 | this.router.navigate(['/project', this.project.id, msgId]); 99 | } 100 | 101 | ngOnDestroy() { 102 | this.subs.forEach(sub => sub.unsubscribe()); 103 | } 104 | 105 | private setTranslation(tr: Translation) { 106 | this.projects.setCurrentTranslation(tr); 107 | } 108 | 109 | private setSource(src: Source) { 110 | this.source = src; 111 | this.projects.setCurrentSource(src); 112 | } 113 | 114 | public exportAs(fmt: 'xmb' | 'xlf') { 115 | let exportFunc; 116 | switch (fmt) { 117 | case 'xmb': exportFunc = Xmb.export; break; 118 | case 'xlf': exportFunc = Xlf.export; break; 119 | } 120 | 121 | const content = exportFunc(this.source, this.translation); 122 | 123 | const blob = new Blob([content], { type: 'application/xml' }); 124 | let filename = this.filename; 125 | if (filename.substr(-3) !== fmt) { 126 | filename = filename.substr(0, filename.length - 3) + fmt; 127 | } 128 | FileSaver.saveAs(blob, filename); 129 | 130 | this.projects.setNeedSave(false); 131 | } 132 | 133 | public onEdit() { 134 | this.router.navigate(['/project', this.project.id, 'edit']); 135 | } 136 | 137 | public canDeactivate(): Observable | boolean { 138 | if (this.needSave) { 139 | const snackBarRef = this.snackBar.open('Changes need to be saved', 'Ignore', { duration: 3000 }); 140 | 141 | const dism$ = snackBarRef.afterDismissed().map(() => false); 142 | const act$ = snackBarRef.onAction().map(() => { 143 | this.projects.setNeedSave(false); // Ignore changes 144 | return true; 145 | }); 146 | return Observable.merge(dism$, act$); 147 | } else { 148 | return true; 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/app/services/backend.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { BackendService } from './backend.service'; 4 | 5 | describe('BackendService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [BackendService] 9 | }); 10 | }); 11 | 12 | it('should ...', inject([BackendService], (service: BackendService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/backend.service.ts: -------------------------------------------------------------------------------- 1 | import { Project } from './../models/project'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | @Injectable() 5 | export class BackendService { 6 | 7 | constructor() { } 8 | 9 | public projectsList(): Project[] { 10 | try { 11 | let ret = JSON.parse(localStorage.getItem('projects')); 12 | if (ret === null) { 13 | ret = []; 14 | } 15 | return ret; 16 | } catch (e) { 17 | return []; 18 | } 19 | } 20 | 21 | public projectsSave(projects: Project[]) { 22 | localStorage.setItem('projects', JSON.stringify(projects)); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/app/services/can-deactivate-project.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { CanDeactivateProjectService } from './can-deactivate-project.service'; 4 | 5 | describe('CanDeactivateProjectService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [CanDeactivateProjectService] 9 | }); 10 | }); 11 | 12 | it('should ...', inject([CanDeactivateProjectService], (service: CanDeactivateProjectService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/can-deactivate-project.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanDeactivate } from '@angular/router'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | export interface CanComponentDeactivate { 6 | canDeactivate: () => Observable | boolean; 7 | } 8 | 9 | @Injectable() 10 | export class CanDeactivateProjectService implements CanDeactivate { 11 | canDeactivate(component: CanComponentDeactivate) { 12 | return component.canDeactivate ? component.canDeactivate() : true; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/services/github.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { GithubService } from './github.service'; 4 | 5 | describe('GithubService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [GithubService] 9 | }); 10 | }); 11 | 12 | it('should ...', inject([GithubService], (service: GithubService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/github.service.ts: -------------------------------------------------------------------------------- 1 | import { Xlf } from './../models/xlf'; 2 | import { Xmb } from './../models/xmb'; 3 | import { Translation } from './../models/translation'; 4 | import { Source, Msg, Location } from './../models/source'; 5 | import { Project } from './../models/project'; 6 | import { Injectable, EventEmitter } from '@angular/core'; 7 | import { Http, Response } from '@angular/http'; 8 | import { Observable } from 'rxjs/Observable'; 9 | import { ReplaySubject } from 'rxjs/ReplaySubject'; 10 | import 'rxjs/add/operator/map'; 11 | 12 | @Injectable() 13 | export class GithubService { 14 | 15 | constructor(private http: Http) { } 16 | 17 | /** 18 | * Get Source 19 | */ 20 | public getSource(p: Project): Observable { 21 | const url = 'https://raw.githubusercontent.com/' + p.repo + '/master/' + p.i18ndir + '/' + p.source; 22 | const obs = new ReplaySubject(0); 23 | obs.next('loading ' + url); 24 | this.http.get(url) // TODO make observable chain 25 | .subscribe( 26 | (resp: Response) => { 27 | const data = resp.text(); 28 | obs.next(true); 29 | obs.next('detecting format (xliff or xmb)'); 30 | const fmt = this.detectFormat(data); 31 | if (fmt === null) { 32 | obs.next(false); 33 | return; 34 | } 35 | obs.next(true); 36 | 37 | let parseFunc; 38 | switch (fmt) { 39 | case 'xmb': parseFunc = Xmb.parseSource; break; 40 | case 'xliff': parseFunc = Xlf.parseSource; break; 41 | } 42 | obs.next('parsing file in ' + fmt + ' format'); 43 | const source = parseFunc(data); 44 | obs.next(source !== null); 45 | if (source === null) { 46 | return; 47 | } 48 | obs.next(source); 49 | }, 50 | err => obs.next(false)); 51 | 52 | return obs; 53 | } 54 | 55 | private detectFormat(data: string): 'xmb' | 'xliff' | null { 56 | try { 57 | const parser = new DOMParser(); 58 | const xmlDoc = parser.parseFromString(data, 'text/xml'); 59 | if (xmlDoc.getElementsByTagName('messagebundle').length) { 60 | return 'xmb'; 61 | } else if (xmlDoc.getElementsByTagName('xliff').length) { 62 | return 'xliff'; 63 | } 64 | return null; 65 | } catch (e) { 66 | return null; 67 | } 68 | } 69 | 70 | /** 71 | * Get translation 72 | * @param p 73 | * @param translation 74 | */ 75 | public getTranslation(p: Project, translation: string): Observable { 76 | const obs = new ReplaySubject(0); 77 | const url = 'https://raw.githubusercontent.com/' + p.repo + '/master/' + p.i18ndir + '/' + translation; 78 | obs.next('loading ' + url); 79 | this.http.get(url) // TODO make observable chain 80 | .subscribe( 81 | (resp: Response) => { 82 | const data = resp.text(); 83 | obs.next(true); 84 | obs.next('detecting format (xliff or xmb)'); 85 | const fmt = this.detectFormat(data); 86 | if (fmt === null) { 87 | obs.next(false); 88 | return; 89 | } 90 | obs.next(true); 91 | 92 | let parseFunc; 93 | switch (fmt) { 94 | case 'xmb': parseFunc = Xmb.parseTranslation; break; 95 | case 'xliff': parseFunc = Xlf.parseTranslation; break; 96 | } 97 | obs.next('parsing file in ' + fmt + ' format'); 98 | const tr = parseFunc(data); 99 | obs.next(tr !== null); 100 | if (tr === null) { 101 | return; 102 | } 103 | obs.next(tr); 104 | }, 105 | err => { 106 | obs.next(true); 107 | const ret: Translation = { msgs: [] }; 108 | obs.next(ret); 109 | }); 110 | return obs; 111 | } 112 | 113 | public getLocation(p: Project, location: Location): Observable { 114 | let filename = location.sourcefile; 115 | if (filename.substr(-3) === '.ts') { 116 | filename = filename.substr(0, filename.length - 3) + '.html'; 117 | } 118 | 119 | const url = 'https://raw.githubusercontent.com/' + p.repo + '/master/src/' + filename; 120 | return this.http.get(url) 121 | .map( 122 | (resp: Response) => { 123 | const data = resp.text(); 124 | const lines = data.split('\n'); 125 | return lines.slice(Math.max(0, location.linenumber - 2), location.linenumber + 1).join('\n'); 126 | }, 127 | err => { 128 | return ''; 129 | }); 130 | 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/app/services/projects.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { ProjectsService } from './projects.service'; 4 | 5 | describe('ProjectsService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [ProjectsService] 9 | }); 10 | }); 11 | 12 | it('should ...', inject([ProjectsService], (service: ProjectsService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/projects.service.ts: -------------------------------------------------------------------------------- 1 | import { Icu } from './../models/icu'; 2 | import { Translation } from './../models/translation'; 3 | import { Source, Msg } from './../models/source'; 4 | import { BackendService } from './backend.service'; 5 | import { Project } from './../models/project'; 6 | import { Injectable } from '@angular/core'; 7 | import { ReplaySubject } from 'rxjs/ReplaySubject'; 8 | 9 | @Injectable() 10 | export class ProjectsService { 11 | 12 | /** 13 | * The list of defined projects 14 | */ 15 | public projects$ = new ReplaySubject(1); 16 | private projects: Project[]; 17 | 18 | /** 19 | * The selected translation file 20 | */ 21 | public currentTranslation$ = new ReplaySubject(1); 22 | private currentTranslation: Translation; 23 | 24 | /** 25 | * The urrent source 26 | */ 27 | public currentSource$ = new ReplaySubject(1); 28 | 29 | /** 30 | * Does the current project need to be saved 31 | */ 32 | public needSave$ = new ReplaySubject(1); 33 | 34 | constructor(private backend: BackendService) { 35 | this.projects = this.backend.projectsList(); 36 | this.projects$.next(this.projects); 37 | } 38 | 39 | public add(p: Project) { 40 | p.id = 1 + this.projects.reduce((prev: number, curr: Project) => Math.max(prev, curr.id), 0); 41 | this.projects.push(p); 42 | this.projects$.next(this.projects); 43 | this.backend.projectsSave(this.projects); 44 | return p.id; 45 | } 46 | 47 | public update(id: number, p: Project) { 48 | this.projects = this.projects.filter((pr: Project) => pr.id !== id); 49 | p.id = id; 50 | this.projects.push(p); 51 | this.projects$.next(this.projects); 52 | this.backend.projectsSave(this.projects); 53 | } 54 | 55 | public delete(id: number) { 56 | this.projects = this.projects.filter((pr: Project) => pr.id !== id); 57 | this.projects$.next(this.projects); 58 | this.backend.projectsSave(this.projects); 59 | } 60 | 61 | public get(id: number): Project { 62 | const list = this.projects.filter((p: Project) => p.id === id); 63 | if (list.length === 1) { 64 | return list[0]; 65 | } else { 66 | return null; 67 | } 68 | } 69 | 70 | public setCurrentTranslation(tr: Translation) { 71 | this.currentTranslation = tr; 72 | this.currentTranslation$.next(tr); 73 | } 74 | 75 | public setTranslatedMsg(id: string, value: string) { 76 | const list = this.currentTranslation.msgs.filter((m: Msg) => m.id === id); 77 | if (list.length > 0) { 78 | list[0].content = value; 79 | list[0].icu = Icu.parse(value); 80 | } else { 81 | this.currentTranslation.msgs.push({ 82 | id: id, 83 | content: value, 84 | icu: Icu.parse(value) 85 | }); 86 | } 87 | this.currentTranslation$.next(this.currentTranslation); 88 | this.needSave$.next(true); 89 | } 90 | 91 | public setCurrentSource(src: Source) { 92 | this.currentSource$.next(src); 93 | } 94 | 95 | public setNeedSave(b: boolean) { 96 | this.needSave$.next(b); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/app/source-msgs-list/source-msgs-list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feloy/angular-translator/f521781718541feb21115a6f7c2ff9b20da68094/src/app/source-msgs-list/source-msgs-list.component.css -------------------------------------------------------------------------------- /src/app/source-msgs-list/source-msgs-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | translatea 5 |

{{msg.content}}

6 |

{{getTranslation(msg.id)}}

7 |
8 |
9 |
-------------------------------------------------------------------------------- /src/app/source-msgs-list/source-msgs-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SourceMsgsListComponent } from './source-msgs-list.component'; 4 | 5 | describe('SourceMsgsListComponent', () => { 6 | let component: SourceMsgsListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SourceMsgsListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SourceMsgsListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/source-msgs-list/source-msgs-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Translation } from './../models/translation'; 2 | import { Project } from './../models/project'; 3 | import { Source, Msg } from './../models/source'; 4 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; 5 | 6 | @Component({ 7 | selector: 'app-source-msgs-list', 8 | templateUrl: './source-msgs-list.component.html', 9 | styleUrls: ['./source-msgs-list.component.css'] 10 | }) 11 | export class SourceMsgsListComponent implements OnInit { 12 | 13 | @Input() source: Source; 14 | @Input() translation: Translation; 15 | @Input() untranslatedOnly: boolean; 16 | @Input() projectId: number; 17 | 18 | constructor() { } 19 | 20 | ngOnInit() { 21 | } 22 | 23 | public getTranslation(id: string): string { 24 | if (this.translation === null) { 25 | return ''; 26 | } 27 | const list = this.translation.msgs 28 | .filter((m: Msg) => m.id === id); 29 | if (list.length > 0) { 30 | return list[0].content; 31 | } else { 32 | return ''; 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/app/widgets/location-details/location-details.component.css: -------------------------------------------------------------------------------- 1 | pre { margin: 4px 0; padding: 4px; background-color: #eee; overflow-x: auto; } -------------------------------------------------------------------------------- /src/app/widgets/location-details/location-details.component.html: -------------------------------------------------------------------------------- 1 |
{{content | async}}
-------------------------------------------------------------------------------- /src/app/widgets/location-details/location-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LocationDetailsComponent } from './location-details.component'; 4 | 5 | describe('LocationDetailsComponent', () => { 6 | let component: LocationDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LocationDetailsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LocationDetailsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/widgets/location-details/location-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable'; 2 | import { ProjectsService } from './../../services/projects.service'; 3 | import { GithubService } from './../../services/github.service'; 4 | import { Location } from './../../models/source'; 5 | import { Component, OnInit, Input } from '@angular/core'; 6 | 7 | @Component({ 8 | selector: 'app-location-details', 9 | templateUrl: './location-details.component.html', 10 | styleUrls: ['./location-details.component.css'] 11 | }) 12 | export class LocationDetailsComponent implements OnInit { 13 | 14 | @Input() location: Location; 15 | @Input() projectId: number; 16 | 17 | public content: Observable; 18 | 19 | constructor(private projects: ProjectsService, private github: GithubService) { } 20 | 21 | ngOnInit() { 22 | const project = this.projects.get(this.projectId); 23 | this.content = this.github.getLocation(project, this.location); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/widgets/progression/progression.component.css: -------------------------------------------------------------------------------- 1 | .buttons { text-align: right; } 2 | .main { margin: 0; } 3 | .main.ok { background-color: #C8E6C9; } 4 | .main.err { background-color: #FFCDD2; } 5 | -------------------------------------------------------------------------------- /src/app/widgets/progression/progression.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | replay 5 | check 6 | close 7 |

{{line.str}}

8 |
9 |
10 |
11 | 12 |
13 |
-------------------------------------------------------------------------------- /src/app/widgets/progression/progression.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProgressionComponent } from './progression.component'; 4 | 5 | describe('ProgressionComponent', () => { 6 | let component: ProgressionComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProgressionComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProgressionComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/widgets/progression/progression.component.ts: -------------------------------------------------------------------------------- 1 | import { Source } from './../../models/source'; 2 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | interface Line { 6 | str: string; 7 | res?: boolean; 8 | } 9 | 10 | @Component({ 11 | selector: 'app-progression', 12 | templateUrl: './progression.component.html', 13 | styleUrls: ['./progression.component.css'] 14 | }) 15 | export class ProgressionComponent implements OnInit { 16 | 17 | @Input() details: Observable; 18 | @Output() close = new EventEmitter(); 19 | public lines: Line[] = []; 20 | private currentLine: Line; 21 | public done = false; 22 | public state: boolean = null; 23 | 24 | constructor() { } 25 | 26 | ngOnInit() { 27 | this.details.subscribe((v: string | boolean) => { 28 | if (typeof v === 'string') { 29 | this.currentLine = { str: v }; 30 | this.lines.push(this.currentLine); 31 | } else if (typeof v === 'boolean') { 32 | this.currentLine.res = v; 33 | this.currentLine = null; 34 | if (v === false) { 35 | this.state = false; 36 | } 37 | } else { 38 | this.done = true; 39 | this.state = true; 40 | } 41 | 42 | }); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feloy/angular-translator/f521781718541feb21115a6f7c2ff9b20da68094/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feloy/angular-translator/f521781718541feb21115a6f7c2ff9b20da68094/src/assets/photo.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feloy/angular-translator/f521781718541feb21115a6f7c2ff9b20da68094/src/favicon.ico -------------------------------------------------------------------------------- /src/i18n-xlf/messages.en.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular Translator 7 | Angular Translator 8 | 9 | 10 | Home 11 | Home 12 | 13 | 14 | New project... 15 | New project... 16 | 17 | 18 | About 19 | About 20 | 21 | 22 | Interface Language 23 | Interface Language 24 | 25 | 26 | Create new project 27 | Create new project 28 | 29 | 30 | edition 31 | edition 32 | 33 | 34 | name 35 | name 36 | 37 | 38 | icon 39 | icon 40 | 41 | 42 | github repo 43 | github repo 44 | 45 | 46 | i18n directory 47 | i18n directory 48 | 49 | 50 | source file name 51 | source file name 52 | 53 | 54 | Delete 55 | Delete 56 | 57 | 58 | Cancel 59 | Cancel 60 | 61 | 62 | Add 63 | Add 64 | 65 | 66 | Save 67 | Save 68 | 69 | 70 | Reload source 71 | Reload source 72 | 73 | 74 | Export current translation 75 | Export current translation 76 | 77 | 78 | Edit project settings 79 | Edit project settings 80 | 81 | 82 | Export as XMB 83 | Export as XMB 84 | 85 | 86 | Export as Xliff 87 | Export as Xliff 88 | 89 | 90 | {VAR_PLURAL, plural, =0 {no message} =1 {1 message} other { messages} } 91 | {VAR_PLURAL, plural, =0 {no message} =1 {1 message} other { messages} } 92 | 93 | 94 | , 95 | , 96 | 97 | 98 | {VAR_PLURAL, plural, =0 {0 translated} =1 {1 translated} other { translated} } 99 | {VAR_PLURAL, plural, =0 {0 translated} =1 {1 translated} other { translated} } 100 | 101 | 102 | Select a translation: 103 | Select a translation: 104 | 105 | 106 | translation 107 | translation 108 | 109 | 110 | Untranslated only 111 | Untranslated only 112 | 113 | 114 | Done 115 | Done 116 | 117 | 118 | Press ctrl-enter to save & go to next untranslated message 119 | Press ctrl-enter to save & go to next untranslated message 120 | 121 | 122 | ICU Builder (prototype) 123 | ICU Builder (prototype) 124 | 125 | 126 | meaning: 127 | meaning: 128 | 129 | 130 | description: 131 | description: 132 | 133 | 134 | Locations: 135 | Locations: 136 | 137 | 138 | view details 139 | view details 140 | 141 | 142 | Save & Next 143 | Save & Next 144 | 145 | 146 | 147 | Angular Translator helps you translating your Angular application. 148 | 149 | Translation applications like this one are generally not specialized for a specific framework. 150 | 151 | This application prefers to specialize in the translation of Angular applications only, so it can integrate perfectly with 152 | the framework. 153 | 154 | How it works 155 | 156 | The ng-xi18n Angular tool is responsible for extracting from the app sources the terms to translate. This tool creates a messages.xmb (or messages.xlf) file containing all the necessary information for the human translator to do his job. 157 | 158 | Angular Translator gets this messages.xmb file and uses it at the reference for the terms that have to be translated. It is called the source file in the app. 159 | 160 | In Angular Translator, you then have to declare the different file names where the different translations for specific languages will be stored. When you begin, you do not need to create these translation files, the app will do that for you. They all called the translations files in the app. 161 | 162 | When you have finished translating for a specific language, you can export the translation file for this language and place it beside your source file in your sources repository. If you decide to work again on the translations for this same language, the translations you have made earlier will be extracted from this translation file. 163 | 164 | When you update your app and add or remove terms to translate and regenerate the messages.xmb source file with ng-xi18n, you do not need to merge the differences to your translation files. Because Angular Translator gets the source file as the reference, it will handle correclty the terms removed or added. 165 | 166 | Usage 167 | 168 | First declare a new project in the application, with New Project... in the sidenav. You will have to declare some information for this new project: 169 | 170 | 171 | name: a name that will appear in the sidenav - be clear and concise, 172 | icon: a name of a Material icon - select one here: https://material.io/icons/, 173 | github repo: the github repository where the app will get the files, for example feloy/angular-translator for translating this app, 174 | i18n directory: the directory inside the repository where source and translations files are placed, src/i18ngenerally, 175 | source file name: the name of the source file, messages.xmb or messages.xlfgenerally, 176 | translations files names: the names of the translations files you want to create, one for each language you want to translate your app in - they do not need to exist in the repository. 177 | 178 | 179 | Once your project created, the app will load the contents of the source file and extract the terms to translate. 180 | 181 | You then have to choose one language you want to work with, based on the list of files you have declared as translations files, using the select listing the different translations files names. 182 | 183 | If the translation file exists in the repository, the app loads it and extracts the translations already done. 184 | 185 | You can now begin to translate the different terms. 186 | 187 | When you are done, you can use the Extract button on the upper-right corner to extract the translations in the current language in a local file. You can now commit this file in your repository. 188 | 189 | 190 | Angular Translator helps you translating your Angular application. 191 | 192 | Translation applications like this one are generally not specialized for a specific framework. 193 | 194 | This application prefers to specialize in the translation of Angular applications only, so it can integrate perfectly with 195 | the framework. 196 | 197 | How it works 198 | 199 | The ng-xi18n Angular tool is responsible for extracting from the app sources the terms to translate. This tool creates a messages.xmb (or messages.xlf) file containing all the necessary information for the human translator to do his job. 200 | 201 | Angular Translator gets this messages.xmb file and uses it at the reference for the terms that have to be translated. It is called the source file in the app. 202 | 203 | In Angular Translator, you then have to declare the different file names where the different translations for specific languages will be stored. When you begin, you do not need to create these translation files, the app will do that for you. They all called the translations files in the app. 204 | 205 | When you have finished translating for a specific language, you can export the translation file for this language and place it beside your source file in your sources repository. If you decide to work again on the translations for this same language, the translations you have made earlier will be extracted from this translation file. 206 | 207 | When you update your app and add or remove terms to translate and regenerate the messages.xmb source file with ng-xi18n, you do not need to merge the differences to your translation files. Because Angular Translator gets the source file as the reference, it will handle correclty the terms removed or added. 208 | 209 | Usage 210 | 211 | First declare a new project in the application, with New Project... in the sidenav. You will have to declare some information for this new project: 212 | 213 | 214 | name: a name that will appear in the sidenav - be clear and concise, 215 | icon: a name of a Material icon - select one here: https://material.io/icons/, 216 | github repo: the github repository where the app will get the files, for example feloy/angular-translator for translating this app, 217 | i18n directory: the directory inside the repository where source and translations files are placed, src/i18ngenerally, 218 | source file name: the name of the source file, messages.xmb or messages.xlfgenerally, 219 | translations files names: the names of the translations files you want to create, one for each language you want to translate your app in - they do not need to exist in the repository. 220 | 221 | 222 | Once your project created, the app will load the contents of the source file and extract the terms to translate. 223 | 224 | You then have to choose one language you want to work with, based on the list of files you have declared as translations files, using the select listing the different translations files names. 225 | 226 | If the translation file exists in the repository, the app loads it and extracts the translations already done. 227 | 228 | You can now begin to translate the different terms. 229 | 230 | When you are done, you can use the Extract button on the upper-right corner to extract the translations in the current language in a local file. You can now commit this file in your repository. 231 | 232 | 233 | 234 | -------------------------------------------------------------------------------- /src/i18n-xlf/messages.fr.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular Translator 7 | Traducteur Angular 8 | 9 | 10 | Home 11 | Accueil 12 | 13 | 14 | New project... 15 | Nouveau projet... 16 | 17 | 18 | About 19 | À propos 20 | 21 | 22 | Interface Language 23 | Langue de l'interface 24 | 25 | 26 | Create new project 27 | Créer un nouveau projet 28 | 29 | 30 | edition 31 | Édition 32 | 33 | 34 | name 35 | nom 36 | 37 | 38 | icon 39 | icône 40 | 41 | 42 | github repo 43 | repo github 44 | 45 | 46 | i18n directory 47 | répertoire i18n 48 | 49 | 50 | source file name 51 | nom fichier source 52 | 53 | 54 | Delete 55 | Supprimer 56 | 57 | 58 | Cancel 59 | Annuler 60 | 61 | 62 | Add 63 | Créer 64 | 65 | 66 | Save 67 | Enregistrer 68 | 69 | 70 | Reload source 71 | Recharger source 72 | 73 | 74 | Export current translation 75 | Exporter traduction en cours 76 | 77 | 78 | Edit project settings 79 | Éditer paramètres projet 80 | 81 | 82 | Export as XMB 83 | Exporter en XMB 84 | 85 | 86 | Export as Xliff 87 | Exporter en Xliff 88 | 89 | 90 | {VAR_PLURAL, plural, =0 {no message} =1 {1 message} other { messages} } 91 | {VAR_PLURAL, plural, =0 {aucun message} =1 {1 message} other { messages} } 92 | 93 | 94 | , 95 | , 96 | 97 | 98 | {VAR_PLURAL, plural, =0 {0 translated} =1 {1 translated} other { translated} } 99 | {VAR_PLURAL, plural, =0 {0 traduit} =1 {1 traduit} other { traduits} } 100 | 101 | 102 | Select a translation: 103 | Sélectionnez une traduction : 104 | 105 | 106 | translation 107 | traduction 108 | 109 | 110 | Untranslated only 111 | Non traduits seulement 112 | 113 | 114 | Done 115 | OK 116 | 117 | 118 | Press ctrl-enter to save & go to next untranslated message 119 | Pressez ctrl-Entrée pour enregistrer & passer au terme non traduit suivant 120 | 121 | 122 | ICU Builder (prototype) 123 | Outil ICU (prototype) 124 | 125 | 126 | meaning: 127 | signification : 128 | 129 | 130 | description: 131 | description : 132 | 133 | 134 | Locations: 135 | Locations : 136 | 137 | 138 | view details 139 | voir détails 140 | 141 | 142 | Save & Next 143 | Enreg & Suivant 144 | 145 | 146 | 147 | Angular Translator helps you translating your Angular application. 148 | 149 | Translation applications like this one are generally not specialized for a specific framework. 150 | 151 | This application prefers to specialize in the translation of Angular applications only, so it can integrate perfectly with 152 | the framework. 153 | 154 | How it works 155 | 156 | The ng-xi18n Angular tool is responsible for extracting from the app sources the terms to translate. This tool creates a messages.xmb (or messages.xlf) file containing all the necessary information for the human translator to do his job. 157 | 158 | Angular Translator gets this messages.xmb file and uses it at the reference for the terms that have to be translated. It is called the source file in the app. 159 | 160 | In Angular Translator, you then have to declare the different file names where the different translations for specific languages will be stored. When you begin, you do not need to create these translation files, the app will do that for you. They all called the translations files in the app. 161 | 162 | When you have finished translating for a specific language, you can export the translation file for this language and place it beside your source file in your sources repository. If you decide to work again on the translations for this same language, the translations you have made earlier will be extracted from this translation file. 163 | 164 | When you update your app and add or remove terms to translate and regenerate the messages.xmb source file with ng-xi18n, you do not need to merge the differences to your translation files. Because Angular Translator gets the source file as the reference, it will handle correclty the terms removed or added. 165 | 166 | Usage 167 | 168 | First declare a new project in the application, with New Project... in the sidenav. You will have to declare some information for this new project: 169 | 170 | 171 | name: a name that will appear in the sidenav - be clear and concise, 172 | icon: a name of a Material icon - select one here: https://material.io/icons/, 173 | github repo: the github repository where the app will get the files, for example feloy/angular-translator for translating this app, 174 | i18n directory: the directory inside the repository where source and translations files are placed, src/i18ngenerally, 175 | source file name: the name of the source file, messages.xmb or messages.xlfgenerally, 176 | translations files names: the names of the translations files you want to create, one for each language you want to translate your app in - they do not need to exist in the repository. 177 | 178 | 179 | Once your project created, the app will load the contents of the source file and extract the terms to translate. 180 | 181 | You then have to choose one language you want to work with, based on the list of files you have declared as translations files, using the select listing the different translations files names. 182 | 183 | If the translation file exists in the repository, the app loads it and extracts the translations already done. 184 | 185 | You can now begin to translate the different terms. 186 | 187 | When you are done, you can use the Extract button on the upper-right corner to extract the translations in the current language in a local file. You can now commit this file in your repository. 188 | 189 | Le traducteur Angular vous aide à traduire votre application Angular. 190 | 191 | Les applications de traduction comme celle-ci ne sont en général pas spécialisées pour un framework particulier. 192 | 193 | Cette application préfère se spécialiser dans la traduction d'applications Angular uniquement, pour pouvoir s'intégrer parfaitement avec le framework. 194 | 195 | Comment ça marche 196 | 197 | L'outil Angular ng-xi18n est reponsable de l'extraction des termes à traduire des sources de l'application. Cet outil crée un fichier messages.xmb (ou messages.xlf) contenant toute l'information nécessaire pour que le traducteur humain puisse faire son travail. 198 | 199 | Le traducteur Angular prend ce fichier messages.xmb et l'utilise comme référence pour les termes devant être traduits. Il est appelé le fichier source dans l'application. 200 | 201 | Dans Le traducteur Angular, vous devez ensuite déclarer les différents noms de fichiers où les différentes traductions pour des langues spécifiques vont être enregistrés. Lorsque vous commencez, vous n'avez pas à créer ces fichiers de traductions, l'application les créera pour vous. Ils sont appelés les fichiers de traductions dans l'application. 202 | 203 | Lorsque vous avez fini la traduction pour une langue donnée, vous pouvez exporter le fichier de traduction pour cette langue et le placer à côté de votre fichier source dans votre dépôt de sources. Si vous décidez de continuer à travailler sur les traductions de cette même langue, les traductions que vous avez faites précédemment seront extraites de ce fichier de traductions. 204 | 205 | Lorsque vous mettez à jour votre application et ajoutez ou enlevez des termes à traduire et regénérez le fichier source messages.xmb avec ng-xi18n, vous n'avez pas à fusionner les différences dans vos fichiers de traductions. Le traducteur Angular prenant le fichier source comme référence, il va correctement prendre en compte les termes ajoutés ou supprimés. 206 | 207 | Utilisation 208 | 209 | Déclarez tout d'abord un nouveau projet dans l'application, avec Nouveau Projet... dans le menu. Vous aurez à déclarer quelques informations pour ce nouveau projet : 210 | 211 | 212 | nom : un nom qui apparaîtra dans le menu, soyez clair et concis, 213 | icône : le nom d'un icône Material - sélectionnez-en un ici : https://material.io/icons/, 214 | dépôt github: le dépôt github où l'application prendra les fichiers, par exemple feloy/angular-translator pour traduire cette application, 215 | répertoire i18n : le répertoire du dépôt dans lequel les fichiers source et traductions sont placés, en général src/i18n, 216 | nom du fichier source : le nom du fichier source, en général messages.xmb ou messages.xlf, 217 | noms des fichiers de traductions : les noms des fichiers de traductions que vous voulez créer, un pour chaque langue dans laquelle vous désirez traduire votre application - ils n'ont pas nécessairement à exister dans le dépôt. 218 | 219 | 220 | Une fois votre projet créé, l'application va lire le contenu du fichier source et extraire les termes à traduire. 221 | 222 | Vous avez alors à choisir une des langues avec laquelle travailler, selon la lste des fichiers que vous avez déclarés comme fichiers de traductions, dans la liste de sélection contenant le différents noms de fichiers de traductions. 223 | 224 | Si le fichier de traductions existe dans le dépôt, l'application le lit et en extrait les traductions déjà faites. 225 | 226 | Vous pouvez maintenant commencer à traduire les différents termes. 227 | 228 | Lorsque vous avez terminé, vous pouvez utiliser le bouton Exporter dans le coin en haut à droite pour extraire les traductions de la langue en cours de traduction dans un fichier local. Vous pouvez ensuite envoyer (commit) ce fichier dans votre dépôt. 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /src/i18n-xlf/messages.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular Translator 7 | 8 | 9 | app/frame/frame.component.ts 10 | 6 11 | 12 | App title 13 | 14 | 15 | Home 16 | 17 | 18 | app/frame-sidenav/frame-sidenav.component.ts 19 | 5 20 | 21 | Back to home page entry in sidenav 22 | 23 | 24 | New project... 25 | 26 | 27 | app/frame-sidenav/frame-sidenav.component.ts 28 | 15 29 | 30 | Add a new project in sidenav 31 | 32 | 33 | About 34 | 35 | 36 | app/frame-sidenav/frame-sidenav.component.ts 37 | 20 38 | 39 | About in sidenav 40 | 41 | 42 | Interface Language 43 | 44 | 45 | app/frame-sidenav/frame-sidenav.component.ts 46 | 23 47 | 48 | subheader in sidenav for interface languages 49 | 50 | 51 | Create new project 52 | 53 | 54 | app/forms/new-project/new-project.component.ts 55 | 1 56 | 57 | Title of form used to create a new project 58 | 59 | 60 | edition 61 | 62 | 63 | app/forms/new-project/new-project.component.ts 64 | 2 65 | 66 | Title of form used to edit project 67 | 68 | 69 | name 70 | 71 | 72 | app/forms/new-project/new-project.component.ts 73 | 7 74 | 75 | name of new project 76 | 77 | 78 | icon 79 | 80 | 81 | app/forms/new-project/new-project.component.ts 82 | 10 83 | 84 | icon of new project 85 | 86 | 87 | github repo 88 | 89 | 90 | app/forms/new-project/new-project.component.ts 91 | 13 92 | 93 | github repo of new project 94 | 95 | 96 | i18n directory 97 | 98 | 99 | app/forms/new-project/new-project.component.ts 100 | 16 101 | 102 | i18n directory of new project 103 | 104 | 105 | source file name 106 | 107 | 108 | app/forms/new-project/new-project.component.ts 109 | 19 110 | 111 | source file name of new project 112 | 113 | 114 | Delete 115 | 116 | 117 | app/forms/new-project/new-project.component.ts 118 | 25 119 | 120 | button to delete project 121 | 122 | 123 | Cancel 124 | 125 | 126 | app/forms/new-project/new-project.component.ts 127 | 26 128 | 129 | 130 | app/forms/msg-edit/msg-edit.component.ts 131 | 44 132 | 133 | button to cancel form edition 134 | 135 | 136 | Add 137 | 138 | 139 | app/forms/new-project/new-project.component.ts 140 | 27 141 | 142 | button to create new project 143 | 144 | 145 | Save 146 | 147 | 148 | app/forms/new-project/new-project.component.ts 149 | 28 150 | 151 | button to save existing project 152 | 153 | 154 | Reload source 155 | 156 | 157 | app/project/project.component.ts 158 | 3 159 | 160 | Title on Reload source icon 161 | 162 | 163 | Export current translation 164 | 165 | 166 | app/project/project.component.ts 167 | 4 168 | 169 | Title on Export icon 170 | 171 | 172 | Edit project settings 173 | 174 | 175 | app/project/project.component.ts 176 | 5 177 | 178 | Title on Edit project icon 179 | 180 | 181 | Export as XMB 182 | 183 | 184 | app/project/project.component.ts 185 | 7 186 | 187 | Menu entry to export translation as XMB 188 | 189 | 190 | Export as Xliff 191 | 192 | 193 | app/project/project.component.ts 194 | 8 195 | 196 | Menu entry to export translation as XLIFF 197 | 198 | 199 | {VAR_PLURAL, plural, =0 {no message} =1 {1 message} other { messages} } 200 | 201 | 202 | app/project/project.component.ts 203 | 19 204 | 205 | number of messages to translate 206 | 207 | 208 | , 209 | 210 | 211 | app/project/project.component.ts 212 | 20 213 | 214 | number of messages translated 215 | 216 | 217 | {VAR_PLURAL, plural, =0 {0 translated} =1 {1 translated} other { translated} } 218 | 219 | 220 | app/project/project.component.ts 221 | 20 222 | 223 | 224 | 225 | Select a translation: 226 | 227 | 228 | app/project/project.component.ts 229 | 25 230 | 231 | label to pick a translation in a select 232 | 233 | 234 | translation 235 | 236 | 237 | app/project/project.component.ts 238 | 26 239 | 240 | placeholder on select translation 241 | 242 | 243 | Untranslated only 244 | 245 | 246 | app/project/project.component.ts 247 | 34 248 | 249 | label for slide toggle 250 | 251 | 252 | Done 253 | 254 | 255 | app/widgets/progression/progression.component.ts 256 | 11 257 | 258 | button to close info 259 | 260 | 261 | Press ctrl-enter to save & go to next untranslated message 262 | 263 | 264 | app/forms/msg-edit/msg-edit.component.ts 265 | 2 266 | 267 | card subtitle explaining how to quickly translate 268 | 269 | 270 | ICU Builder (prototype) 271 | 272 | 273 | app/forms/msg-edit/msg-edit.component.ts 274 | 6 275 | 276 | label for slide toggle for icu builder 277 | 278 | 279 | meaning: 280 | 281 | 282 | app/forms/msg-edit/msg-edit.component.ts 283 | 25 284 | 285 | short label before term meaning 286 | 287 | 288 | description: 289 | 290 | 291 | app/forms/msg-edit/msg-edit.component.ts 292 | 26 293 | 294 | short label before term description 295 | 296 | 297 | Locations: 298 | 299 | 300 | app/forms/msg-edit/msg-edit.component.ts 301 | 30 302 | 303 | short label before term locations list 304 | 305 | 306 | view details 307 | 308 | 309 | app/forms/msg-edit/msg-edit.component.ts 310 | 31 311 | 312 | label for slide toggle for locations details 313 | 314 | 315 | Save & Next 316 | 317 | 318 | app/forms/msg-edit/msg-edit.component.ts 319 | 45 320 | 321 | button to save translation 322 | 323 | 324 | 325 | Angular Translator helps you translating your Angular application. 326 | 327 | Translation applications like this one are generally not specialized for a specific framework. 328 | 329 | This application prefers to specialize in the translation of Angular applications only, so it can integrate perfectly with 330 | the framework. 331 | 332 | How it works 333 | 334 | The ng-xi18n Angular tool is responsible for extracting from the app sources the terms to translate. This tool creates a messages.xmb (or messages.xlf) file containing all the necessary information for the human translator to do his job. 335 | 336 | Angular Translator gets this messages.xmb file and uses it at the reference for the terms that have to be translated. It is called the source file in the app. 337 | 338 | In Angular Translator, you then have to declare the different file names where the different translations for specific languages will be stored. When you begin, you do not need to create these translation files, the app will do that for you. They all called the translations files in the app. 339 | 340 | When you have finished translating for a specific language, you can export the translation file for this language and place it beside your source file in your sources repository. If you decide to work again on the translations for this same language, the translations you have made earlier will be extracted from this translation file. 341 | 342 | When you update your app and add or remove terms to translate and regenerate the messages.xmb source file with ng-xi18n, you do not need to merge the differences to your translation files. Because Angular Translator gets the source file as the reference, it will handle correclty the terms removed or added. 343 | 344 | Usage 345 | 346 | First declare a new project in the application, with New Project... in the sidenav. You will have to declare some information for this new project: 347 | 348 | 349 | name: a name that will appear in the sidenav - be clear and concise, 350 | icon: a name of a Material icon - select one here: https://material.io/icons/, 351 | github repo: the github repository where the app will get the files, for example feloy/angular-translator for translating this app, 352 | i18n directory: the directory inside the repository where source and translations files are placed, src/i18ngenerally, 353 | source file name: the name of the source file, messages.xmb or messages.xlfgenerally, 354 | translations files names: the names of the translations files you want to create, one for each language you want to translate your app in - they do not need to exist in the repository. 355 | 356 | 357 | Once your project created, the app will load the contents of the source file and extract the terms to translate. 358 | 359 | You then have to choose one language you want to work with, based on the list of files you have declared as translations files, using the select listing the different translations files names. 360 | 361 | If the translation file exists in the repository, the app loads it and extracts the translations already done. 362 | 363 | You can now begin to translate the different terms. 364 | 365 | When you are done, you can use the Extract button on the upper-right corner to extract the translations in the current language in a local file. You can now commit this file in your repository. 366 | 367 | 368 | 369 | app/home/home.component.ts 370 | 1 371 | 372 | 373 | 374 | 375 | 376 | -------------------------------------------------------------------------------- /src/i18n-xmb/messages.xmb: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ]> 23 | 24 | app/frame/frame.component.ts:6Angular Translator 25 | app/frame-sidenav/frame-sidenav.component.ts:5Home 26 | app/frame-sidenav/frame-sidenav.component.ts:15New project... 27 | app/frame-sidenav/frame-sidenav.component.ts:20About 28 | app/frame-sidenav/frame-sidenav.component.ts:23Interface Language 29 | app/forms/new-project/new-project.component.ts:1Create new project 30 | app/forms/new-project/new-project.component.ts:2INTERPOLATION edition 31 | app/forms/new-project/new-project.component.ts:7name 32 | app/forms/new-project/new-project.component.ts:10icon 33 | app/forms/new-project/new-project.component.ts:13github repo 34 | app/forms/new-project/new-project.component.ts:16i18n directory 35 | app/forms/new-project/new-project.component.ts:19source file name 36 | app/forms/new-project/new-project.component.ts:25Delete 37 | app/forms/new-project/new-project.component.ts:26app/forms/msg-edit/msg-edit.component.ts:44Cancel 38 | app/forms/new-project/new-project.component.ts:27Add 39 | app/forms/new-project/new-project.component.ts:28Save 40 | app/project/project.component.ts:3Reload source 41 | app/project/project.component.ts:4Export current translation 42 | app/project/project.component.ts:5Edit project settings 43 | app/project/project.component.ts:7Export as XMB 44 | app/project/project.component.ts:8Export as Xliff 45 | app/project/project.component.ts:19{VAR_PLURAL, plural, =0 {no message} =1 {1 message} other {INTERPOLATION messages} } 46 | app/project/project.component.ts:20, ICU 47 | app/project/project.component.ts:20{VAR_PLURAL, plural, =0 {0 translated} =1 {1 translated} other {INTERPOLATION translated} } 48 | app/project/project.component.ts:25Select a translation: 49 | app/project/project.component.ts:26translation 50 | app/project/project.component.ts:34Untranslated only 51 | app/widgets/progression/progression.component.ts:11Done 52 | app/forms/msg-edit/msg-edit.component.ts:2Press ctrl-enter to save & go to next untranslated message 53 | app/forms/msg-edit/msg-edit.component.ts:6ICU Builder (prototype) 54 | app/forms/msg-edit/msg-edit.component.ts:25meaning: 55 | app/forms/msg-edit/msg-edit.component.ts:26description: 56 | app/forms/msg-edit/msg-edit.component.ts:30Locations: 57 | app/forms/msg-edit/msg-edit.component.ts:31view details 58 | app/forms/msg-edit/msg-edit.component.ts:45Save & Next 59 | 60 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularTranslator 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.server.ts: -------------------------------------------------------------------------------- 1 | export { AppServerModule } from './app/app.server.module'; 2 | -------------------------------------------------------------------------------- /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().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /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/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | import 'core-js/es7/reflect'; 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '~@angular/material/theming'; 3 | @include mat-core(); 4 | 5 | @import url(https://fonts.googleapis.com/icon?family=Material+Icons); 6 | // base styles 7 | 8 | $primary: mat-palette($mat-indigo); 9 | $accent: mat-palette($mat-pink); 10 | $theme: mat-light-theme($primary, $accent); 11 | @include angular-material-theme($theme); 12 | 13 | body { 14 | font-family: 'Roboto', sans-serif; 15 | } 16 | 17 | html, body, md-sidenav-container, .app-fullpage { 18 | margin: 0; 19 | width: 100%; 20 | height: 100%; 21 | } 22 | 23 | * { 24 | -webkit-font-smoothing: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | } 27 | md-sidenav { 28 | width: 240px; 29 | } 30 | 31 | md-sidenav-layout { background-color: #eceff1; } 32 | 33 | .app-toolbar { position: fixed; z-index: 2; box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.3); } 34 | 35 | .app-fill-remaining-space { 36 | flex: 1 1 auto; 37 | } 38 | 39 | // Elements directly below fixed toolbar 40 | .app-belowtoolbar { margin-top: 64px; } 41 | 42 | md-list-item.active { 43 | background-color: #eceff1; 44 | .set-active { 45 | font-weight: bold; 46 | color: mat-color($accent); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare var __karma__: any; 17 | declare var require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.app-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "baseUrl": "", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ], 13 | "angularCompilerOptions": { 14 | "entryModule": "app/app.server.module#AppServerModule" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "baseUrl": "", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2016", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true, "rxjs"], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-construct": true, 44 | "no-debugger": true, 45 | "no-duplicate-variable": true, 46 | "no-empty": false, 47 | "no-empty-interface": true, 48 | "no-eval": true, 49 | "no-inferrable-types": [true, "ignore-params"], 50 | "no-shadowed-variable": true, 51 | "no-string-literal": false, 52 | "no-string-throw": true, 53 | "no-switch-case-fall-through": true, 54 | "no-trailing-whitespace": true, 55 | "no-unused-expression": true, 56 | "no-use-before-declare": true, 57 | "no-var-keyword": true, 58 | "object-literal-sort-keys": false, 59 | "one-line": [ 60 | true, 61 | "check-open-brace", 62 | "check-catch", 63 | "check-else", 64 | "check-whitespace" 65 | ], 66 | "prefer-const": true, 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "radix": true, 72 | "semicolon": [ 73 | "always" 74 | ], 75 | "triple-equals": [ 76 | true, 77 | "allow-null-check" 78 | ], 79 | "typedef-whitespace": [ 80 | true, 81 | { 82 | "call-signature": "nospace", 83 | "index-signature": "nospace", 84 | "parameter": "nospace", 85 | "property-declaration": "nospace", 86 | "variable-declaration": "nospace" 87 | } 88 | ], 89 | "typeof-compare": true, 90 | "unified-signatures": true, 91 | "variable-name": false, 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ], 100 | 101 | "directive-selector": [true, "attribute", "app", "camelCase"], 102 | "component-selector": [true, "element", "app", "kebab-case"], 103 | "use-input-property-decorator": true, 104 | "use-output-property-decorator": true, 105 | "use-host-property-decorator": true, 106 | "no-input-rename": true, 107 | "no-output-rename": true, 108 | "use-life-cycle-interface": true, 109 | "use-pipe-transform-interface": true, 110 | "component-class-suffix": true, 111 | "directive-class-suffix": true, 112 | "no-access-missing-member": true, 113 | "templates-use-public": true, 114 | "invoke-injectable": true 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const ngtools = require('@ngtools/webpack'); 2 | module.exports = { 3 | entry: { 4 | main: './main.server.ts' 5 | }, 6 | resolve: { 7 | extensions: ['.ts', '.js'] 8 | }, 9 | target: 'node', 10 | output: { 11 | path: __dirname + '/dist', 12 | filename: '[name].js' 13 | }, 14 | plugins: [ 15 | new ngtools.AotPlugin({ 16 | tsConfigPath: './tsconfig.json', 17 | }) 18 | ], 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.ts$/, 23 | loaders: ['@ngtools/webpack', 'angular2-template-loader'], 24 | }, 25 | { 26 | test: /\.(html|css)$/, 27 | loader: 'raw-loader' 28 | }, 29 | { 30 | test: /@angular(\\|\/)material/, 31 | loader: 'imports-loader', 32 | options: { 33 | window: '>global', 34 | 'CSS': '>null', 35 | navigator: '>{get userAgent(){ return \'Chrome\'; }}', 36 | document: '>global.document', 37 | }, 38 | }, 39 | ] 40 | } 41 | } 42 | --------------------------------------------------------------------------------