├── .babelrc ├── .codecov.yml ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .nycrc ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── Server.js ├── appveyor.yml ├── db.config.js ├── docker-compose.yml ├── docs ├── cli-v1.0.md ├── history.md ├── import.md ├── merge.md ├── rest-api.md └── vis.md ├── ktm.config.js ├── ktm.server.config.js ├── less └── app.less ├── nodemon.json ├── package.json ├── packages ├── keys-translations-manager-cli │ ├── .npmignore │ ├── README.md │ ├── bin │ │ └── ktm.js │ └── package.json └── keys-translations-manager-core │ ├── README.md │ ├── index.js │ ├── lib │ ├── historyUtil.js │ ├── importUtil.js │ ├── localeUtil.js │ ├── logUtil.js │ ├── mergeUtil.js │ ├── timingUtil.js │ └── transformationUtil.js │ └── package.json ├── public ├── image │ └── favicon.ico └── locale │ ├── en-US │ └── translation.json │ ├── zh-CN │ └── translation.json │ └── zh-TW │ └── translation.json ├── src ├── App.jsx ├── actions │ ├── components.js │ ├── counts.js │ ├── errors.js │ ├── history.js │ ├── keys.js │ ├── messages.js │ ├── socket.js │ ├── translations.js │ └── vis.js ├── client │ └── index.js ├── components │ ├── grid │ │ ├── ActionCellRenderer.jsx │ │ ├── ConfirmModal.jsx │ │ └── TablePanel.jsx │ ├── history │ │ ├── DiffPanel.jsx │ │ └── HistoryModal.jsx │ ├── import │ │ └── ImportModal.jsx │ ├── input │ │ ├── AlertPanel.jsx │ │ ├── EditModal.jsx │ │ ├── FormPanel.jsx │ │ ├── InputPanel.jsx │ │ └── TextField.jsx │ ├── layout │ │ ├── DropdownMenu.jsx │ │ ├── Header.jsx │ │ ├── MainPanel.jsx │ │ ├── Mask.jsx │ │ ├── MessagePopup.jsx │ │ ├── SideBar.jsx │ │ └── Spinner.jsx │ ├── merge │ │ └── MergeModal.jsx │ ├── output │ │ ├── CountCol.jsx │ │ ├── FileTypeCol.jsx │ │ └── OutputPanel.jsx │ └── vis │ │ ├── Tooltip.jsx │ │ └── Tree.jsx ├── configUtil.js ├── constants │ ├── ActionTypes.js │ ├── InitStates.js │ ├── Languages.js │ └── Status.js ├── containers │ ├── RootContainer.js │ └── VisContainer.js ├── controllers │ ├── CountController.js │ ├── DownloadController.js │ ├── HistoryController.js │ ├── ImportController.js │ ├── KeyController.js │ ├── TranslationController.js │ └── VisController.js ├── models │ ├── HistoryModel.js │ └── TranslationModel.js ├── reducers │ ├── components.js │ ├── counts.js │ ├── errors.js │ ├── history.js │ ├── index.js │ ├── messages.js │ ├── socket.js │ ├── translations.js │ └── vis.js ├── server │ └── index.js └── store │ └── configureStore.js ├── tests ├── mock │ ├── translation │ ├── translation.json │ └── translation.properties ├── packages │ └── keys-translations-manager-core │ │ └── lib │ │ ├── historyUtil.js │ │ ├── importUtil.js │ │ ├── localeUtil.js │ │ ├── logUtil.js │ │ ├── mergeUtil.js │ │ ├── timingUtil.js │ │ └── transformationUtil.js ├── src │ ├── actions │ │ ├── components.js │ │ ├── counts.js │ │ ├── errors.js │ │ ├── keys.js │ │ ├── messages.js │ │ ├── socket.js │ │ ├── translations.js │ │ └── vis.js │ ├── components │ │ ├── grid │ │ │ ├── ConfirmModal.js │ │ │ └── TablePanel.js │ │ ├── import │ │ │ └── ImportModal.js │ │ ├── input │ │ │ ├── AlertPanel.js │ │ │ ├── EditModal.js │ │ │ ├── FormPanel.js │ │ │ ├── InputPanel.js │ │ │ └── TextField.js │ │ ├── layout │ │ │ ├── DropdownMenu.js │ │ │ ├── Header.js │ │ │ ├── MainPanel.js │ │ │ ├── Mask.js │ │ │ ├── MessagePopup.js │ │ │ ├── SideBar.js │ │ │ └── Spinner.js │ │ ├── merge │ │ │ └── MergeModal.js │ │ ├── output │ │ │ ├── CountCol.js │ │ │ ├── FileTypeCol.js │ │ │ └── OutputPanel.js │ │ └── vis │ │ │ ├── Tooltip.js │ │ │ └── Tree.js │ └── reducers │ │ ├── components.js │ │ ├── counts.js │ │ ├── errors.js │ │ ├── messages.js │ │ ├── socket.js │ │ ├── translations.js │ │ └── vis.js └── testHelper.js ├── views └── index.ejs ├── webpack.config.dev.js ├── webpack.config.prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties" 8 | ], 9 | "env": { 10 | "test": { 11 | "plugins": ["istanbul"] 12 | }, 13 | "development": { 14 | "plugins": ["react-hot-loader/babel"] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | # basic 6 | target: 85% 7 | threshold: 3% 8 | base: auto 9 | patch: 10 | default: 11 | # basic 12 | target: 85% 13 | threshold: 3% 14 | base: auto -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | coverage 2 | docs 3 | logs 4 | node_modules 5 | packages 6 | public/css 7 | public/js 8 | tests 9 | *.yml 10 | *ignore 11 | .editorconfig 12 | .eslintrc 13 | *.md 14 | *.map 15 | *.bat 16 | *.log 17 | Dockerfile 18 | LICENSE 19 | webpack.config.dev.js 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | 7 | [*.{js,json,css,less}] 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": ["babel", "react", "i18n", "import"], 4 | "extends": "eslint:recommended", 5 | "rules": { 6 | "accessor-pairs": 2, 7 | "block-scoped-var": 2, 8 | "complexity": 0, 9 | "consistent-return": 2, 10 | "curly": 2, 11 | "default-case": 2, 12 | "dot-notation": 2, 13 | "dot-location": [2, "property"], 14 | "eqeqeq": 2, 15 | "guard-for-in": 2, 16 | "no-alert": 2, 17 | "no-caller": 2, 18 | "no-div-regex": 2, 19 | "no-else-return": 2, 20 | "no-eq-null": 2, 21 | "no-eval": 2, 22 | "no-extend-native": 2, 23 | "no-extra-bind": 2, 24 | "no-fallthrough": 2, 25 | "no-floating-decimal": 2, 26 | "no-implicit-coercion": 0, 27 | "no-implied-eval": 2, 28 | "no-invalid-this": 2, 29 | "no-iterator": 2, 30 | "no-labels": 2, 31 | 32 | "no-loop-func": 2, 33 | "no-multi-spaces": 2, 34 | "no-multi-str": 2, 35 | "no-native-reassign": 2, 36 | "no-new-func": 2, 37 | "no-new-wrappers": 2, 38 | "no-new": 2, 39 | "no-octal-escape": 2, 40 | "no-octal": 2, 41 | "no-param-reassign": 0, 42 | "no-process-env": 0, 43 | "no-proto": 2, 44 | "no-redeclare": [2, { 45 | "builtinGlobals": true 46 | }], 47 | "no-return-assign": [2, "always"], 48 | "no-script-url": 2, 49 | "no-self-compare": 0, 50 | "no-sequences": 2, 51 | "no-throw-literal": 2, 52 | "no-unused-expressions": 2, 53 | "no-useless-call": 2, 54 | "no-useless-concat": 2, 55 | "no-useless-constructor": 2, 56 | "no-void": 0, 57 | "no-warning-comments": [1, { 58 | "terms": [ 59 | "todo", 60 | "warning", 61 | "fixme", 62 | "hack", 63 | "xxx" 64 | ], 65 | "location": "start" 66 | }], 67 | "no-with": 2, 68 | "radix": 2, 69 | "vars-on-top": 2, 70 | "wrap-iife": [ 2, "inside"], 71 | "yoda": [ 2, "never", { 72 | "exceptRange": true 73 | }], 74 | 75 | "no-console": 0, 76 | 77 | "i18n/no-chinese-character": 2, 78 | 79 | "import/no-unresolved": 2, 80 | "import/export": 2, 81 | "import/no-named-as-default": 2, 82 | "import/no-named-as-default-member": 2, 83 | "import/no-deprecated": 2, 84 | "import/no-extraneous-dependencies": 2, 85 | "import/no-mutable-exports": 2, 86 | 87 | "react/jsx-no-duplicate-props": 2, 88 | "react/jsx-no-undef": 2, 89 | "react/jsx-uses-react": 2, 90 | "react/jsx-uses-vars": 2, 91 | "react/no-danger": 2, 92 | "react/no-deprecated": 2, 93 | "react/no-did-mount-set-state": 2, 94 | "react/no-did-update-set-state": 2, 95 | "react/no-direct-mutation-state": 2, 96 | "react/no-is-mounted": 2, 97 | "react/no-unknown-property": 2, 98 | "react/prefer-es6-class": 2, 99 | "react/prop-types": 2, 100 | "react/react-in-jsx-scope": 2, 101 | "react/self-closing-comp": 2, 102 | "react/sort-comp": 2, 103 | "react/jsx-wrap-multilines": 2 104 | }, 105 | "settings": { 106 | "react": { 107 | "version": "detect" 108 | }, 109 | "import/resolver": { 110 | "node": { 111 | "extensions": [".js",".jsx"] 112 | } 113 | } 114 | }, 115 | "env": { 116 | "browser": true, 117 | "es6": true, 118 | "node": true, 119 | "jquery": true 120 | }, 121 | "parserOptions": { 122 | "ecmaVersion": 6, 123 | "sourceType": "module", 124 | "ecmaFeatures": { 125 | "arrowFunctions": true, 126 | "classes": true, 127 | "modules": true, 128 | "spread": true, 129 | "superInFunctions": true, 130 | "jsx": true 131 | } 132 | }, 133 | "globals": { 134 | "__DEV__": true 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | logs 4 | node_modules 5 | public/js 6 | app.css 7 | *.map 8 | *.bat 9 | *.log 10 | *.bak 11 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "src/components/vis/Tree.jsx" 4 | ], 5 | "extends": "@istanbuljs/nyc-config-babel" 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | after_success: 5 | - npm run coverage 6 | - npm run codecov 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.11.0-alpine3.12 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json yarn.lock /app/ 6 | RUN yarn && yarn cache clean && apk add nano 7 | 8 | COPY . /app 9 | RUN yarn build 10 | 11 | EXPOSE 3000 12 | 13 | CMD ["yarn", "start"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Chang, Che-Jen 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | nodejs_version: "10" 3 | 4 | install: 5 | - ps: Install-Product node $env:nodejs_version 6 | - npm install 7 | 8 | test_script: 9 | - node --version 10 | - npm --version 11 | - npm test 12 | 13 | build: off 14 | -------------------------------------------------------------------------------- /db.config.js: -------------------------------------------------------------------------------- 1 | module.exports = 'mongodb://localhost:27017/translationdb'; 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | ktm-web: 5 | image: ktm/express 6 | container_name: ktm-web 7 | restart: always 8 | build: . 9 | environment: 10 | - DB=mongodb://ktm-db:27017/translationdb 11 | ports: 12 | - "3000:3000" 13 | depends_on: 14 | - ktm-db 15 | ktm-db: 16 | image: mongo 17 | container_name: ktm-db 18 | restart: always 19 | volumes: 20 | - /data/db:/data/db 21 | ports: 22 | - "27017:27017" 23 | -------------------------------------------------------------------------------- /docs/cli-v1.0.md: -------------------------------------------------------------------------------- 1 | # keys-translations-manager-cli (v1.0) 2 | 3 | 4 | ## Installation 5 | Global installation: 6 | ```sh 7 | $ npm install -g keys-translations-manager-cli 8 | ``` 9 | 10 | Local installation: 11 | ```sh 12 | $ npm install --save-dev keys-translations-manager-cli 13 | ``` 14 | 15 | ## Configuration 16 | Add `.ktmrc` to your home directory (or you can add `.ktmrc` into your project if you installed the cli tool locally.) 17 | 18 | * Sample `.ktmrc`: 19 | ```json 20 | { 21 | "database": "mongodb://localhost:27017/translationdb", 22 | "output": { 23 | "filename": "translation", 24 | "path": "/path/to/output/${locale}" 25 | } 26 | } 27 | ``` 28 | * `${locale}` can be a placeholder for **filename** and/or **path**. 29 | 30 | 31 | ## Usage 32 | ktm [locale1 (, locale2, ...)] -t [json | properties] -p [project ID] 33 | 34 | 35 | ## Options 36 | 37 | | Option | Shorthand | Description | Required | 38 | |:------------|:---------------:|:-----|:-----:| 39 | | --type | -t | Specify a file type.
(Please provide either `json` or `properties`) | Y | 40 | | --project | -p | Specify a project to output locales.
(Please provide **a project ID**) | Y | 41 | | --format | -f | Sort keys alphabetically. | 42 | | --help | -h | Show help. | 43 | 44 | * You have to map **project ID** to the setting in [ktm.config.js](https://github.com/chejen/keys-translations-manager/blob/develop/ktm.config.js) (at [keys-translations-manager](https://github.com/chejen/keys-translations-manager)). 45 | 46 | 47 | ## Example 48 | If you globally installed the cli tool, just execute the command like this: 49 | ```sh 50 | $ ktm en-US zh-TW -p p1 -t json --format 51 | ``` 52 | Or, if you had it installed locally by your project, you can add `ktm` script to package.json's **scripts** property, 53 | ```js 54 | "scripts": { 55 | "ktm": "ktm en-US zh-TW -p p1 -t json --format" 56 | } 57 | ``` 58 | then execute: 59 | ```sh 60 | $ npm run ktm 61 | ``` 62 | 63 | Finally, you will get your outputs like these: 64 | * /path/to/output/en-US/translation.json 65 | * /path/to/output/zh-TW/translation.json 66 | -------------------------------------------------------------------------------- /docs/history.md: -------------------------------------------------------------------------------- 1 | ## Revision History Demonstration 2 | 3 | * Click the history icon on the data row. 4 | ![step1](https://user-images.githubusercontent.com/14872888/46097564-e0a6aa00-c1f4-11e8-89b1-83b3baa4f37e.png) 5 | 6 | * Then a pop-up modal with the key's revision history will show up. 7 | ![step2](https://user-images.githubusercontent.com/14872888/46097654-092ea400-c1f5-11e8-9475-c999c2582402.png) 8 | -------------------------------------------------------------------------------- /docs/import.md: -------------------------------------------------------------------------------- 1 | ## Import Instructions 2 | 3 | * Prerequisite: Prepare your locales to import (or you can get [locales](https://github.com/chejen/keys-translations-manager/tree/master/public/locale) from this project for a trial.) 4 | 5 | * Step 1: Click the import button on the upper-right corner to open a popup. 6 | 7 | ![step1](https://user-images.githubusercontent.com/14872888/46032442-395f3f80-c12e-11e8-8263-1ebbf1abb221.png) 8 | 9 | * Step 2: Choose a file, select locale and project to import, then hit the '**Imort**' button to submit. 10 | 11 | ![step2](https://cloud.githubusercontent.com/assets/14872888/17220530/46ca8988-5522-11e6-9e75-2405ad0dfb21.png) 12 | 13 | > Only `*.json` or `*.properties` will be accepted by Keys-Translations Manager. 14 | 15 | * Step 3: Let system check if the data imported is valid. If so, you will see the result like this: 16 | 17 | ![step3](https://user-images.githubusercontent.com/14872888/46030069-b76c1800-c127-11e8-86a4-fccc27b88d3c.png) 18 | 19 | > Keys-Translations Manager will help you validate your data, and here are 2 main principles: **(1)** The field `Key` is unique by {project + locale}. **(2)** If the key "ui.common.add" exists, "ui", "ui.common", and "ui.common.add.*" are all disallowed. Otherwise, it would cause JSON parsing errors. 20 | 21 | * Step 4: Repeat step 1 to step 3 until all of your locales are imported. 22 | 23 | ![step4](https://user-images.githubusercontent.com/14872888/46030067-b76c1800-c127-11e8-93d9-ea78f9e6e93f.png) 24 | -------------------------------------------------------------------------------- /docs/merge.md: -------------------------------------------------------------------------------- 1 | ## Merge Instructions 2 | 3 | > MERGE functionality merges the same keys that sit in different projects but have translations all the same in every locale. 4 | 5 | * Step 1: Click the merge button on the upper-right corner to open a popup. 6 | 7 | ![step1](https://user-images.githubusercontent.com/14872888/46032131-5cd5ba80-c12d-11e8-8757-b58715ea19d1.png) 8 | 9 | * Step 2: Confirm if you want to merge these keys. If so, hit the '**Yes**' button. 10 | 11 | ![step2](https://cloud.githubusercontent.com/assets/14872888/17220526/46a3291a-5522-11e6-8b78-368a667437a8.png) 12 | 13 | * Step3: The keys are merged. 14 | 15 | ![step3](https://user-images.githubusercontent.com/14872888/46032134-5cd5ba80-c12d-11e8-81bf-954e10edb0a0.png) 16 | -------------------------------------------------------------------------------- /docs/rest-api.md: -------------------------------------------------------------------------------- 1 | ## REST API 2 | 3 | #### /api/rest/{format}/{fileType}/{projectId}/{locale} 4 | 5 | * Method: 6 | * GET 7 | 8 | * Parameters: 9 | * `format`: replace it with **f** (formatted) or **n** (not formatted) 【Required】 10 | * `fileType`: replace it with **json** (nested JSON), **flat** (flat JSON) or **properties** 【Required】 11 | * `projectId`: replace it with the project ID set in [ktm.config.js](https://github.com/chejen/keys-translations-manager/blob/master/ktm.config.js) 【Required】 12 | * `locale`: replace it with the locale set in [ktm.config.js](https://github.com/chejen/keys-translations-manager/blob/master/ktm.config.js) 13 | 14 | Example request URIs: 15 | 16 | * GET http://localhost:3000/api/rest/n/properties/p1/zh-TW 17 | * Fetch zh-TW locale in Property format from project p1. 18 | 19 | * GET http://localhost:3000/api/rest/f/json/p1/en-US 20 | * Fetch en-US locale with alphabetically sorted JSON from project p1. 21 | 22 | 23 | #### /api/download/{format}/{fileType}/{projectId}/{locale} 24 | 25 | * Method: 26 | * GET 27 | 28 | * Parameters: 29 | * `format`: replace it with **f** (formatted) or **n** (not formatted) 【Required】 30 | * `fileType`: replace it with **json** (nested JSON), **flat** (flat JSON) or **properties** 【Required】 31 | * `projectId`: replace it with the project ID set in [ktm.config.js](https://github.com/chejen/keys-translations-manager/blob/master/ktm.config.js) 【Required】 32 | * `locale`: replace it with the locale set in [ktm.config.js](https://github.com/chejen/keys-translations-manager/blob/master/ktm.config.js) 33 | 34 | Example request URIs: 35 | 36 | * GET http://localhost:3000/api/download/n/properties/p1/zh-TW 37 | * Download a Property-formatted zh-TW locale from project p1. 38 | 39 | * GET http://localhost:3000/api/download/f/json/p1/en-US 40 | * Download a JSON-formatted en-US locale from project p1. 41 | 42 | * GET http://localhost:3000/api/download/n/properties/p1 43 | * Download a ZIP file containing all of the locales in project p1. 44 | -------------------------------------------------------------------------------- /docs/vis.md: -------------------------------------------------------------------------------- 1 | ## Visualization Demonstration 2 | 3 | * Click the counter to navigate to visualization. 4 | ![step1](https://user-images.githubusercontent.com/14872888/46031572-cc4aaa80-c12b-11e8-838e-37a11da88dcd.png) 5 | 6 | * Visualization displays the keys and the translations with D3 tree layout. (By default, the tree expands only level one nodes.) 7 | ![step2](https://user-images.githubusercontent.com/14872888/46030562-0a929a80-c129-11e8-9c3d-68dcbe9af84f.png) 8 | 9 | * Click a solid circle to expand its child nodes. 10 | ![step3](https://user-images.githubusercontent.com/14872888/46030559-09fa0400-c129-11e8-89b9-bf98e251fac1.png) 11 | 12 | * Zoom in and out using the mouse wheel; reset zoom by clicking the "Reset" button. 13 | ![step4](https://user-images.githubusercontent.com/14872888/46030560-0a929a80-c129-11e8-86be-2ffb23c0e6d9.png) 14 | 15 | * Hover over the hollow-circle leaf to show translations. 16 | ![step5](https://user-images.githubusercontent.com/14872888/46030561-0a929a80-c129-11e8-851d-01f8628ecfc0.png) 17 | -------------------------------------------------------------------------------- /ktm.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * If you change the configurations, 4 | * don't forget to rebuild the code (npm run build) and 5 | * restart the server (npm run start). 6 | */ 7 | locales: ['en-US', 'zh-TW'], 8 | projects: [ // make sure the ids are 'String' type 9 | { id: 'p1', name: 'Project A' }, 10 | { id: 'p2', name: 'Project B' } 11 | ], 12 | enableNotifications: true 13 | }; 14 | -------------------------------------------------------------------------------- /ktm.server.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Sample corsWhitelist (accept RegExp or String): 3 | // corsWhitelist: [/127.0.0.1/, 'http://localhost:3000'] 4 | }; 5 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ext": "*", 4 | "ignore": [ 5 | ".git", 6 | "node_modules/**/node_modules", 7 | "docs/", 8 | "public/", 9 | "src/actions/", 10 | "src/client/", 11 | "src/components/", 12 | "src/constants/", 13 | "src/containers/", 14 | "src/reducers/", 15 | "src/server/", 16 | "src/store/", 17 | "src/App.js", 18 | "src/configUtil.js", 19 | "src/routes.js", 20 | "tests/" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keys-translations-manager", 3 | "version": "1.6.0", 4 | "private": true, 5 | "description": "A web application for locale's keys and translations management.", 6 | "keywords": [ 7 | "i18n", 8 | "internationalization", 9 | "lang", 10 | "language", 11 | "l10n", 12 | "locale", 13 | "localization", 14 | "multilingual", 15 | "translation", 16 | "MERN" 17 | ], 18 | "author": "chejen", 19 | "scripts": { 20 | "lint": "eslint --ext .js,.jsx src/**", 21 | "dev": "cross-env NODE_ENV=development nodemon --exec babel-node -- ./Server.js", 22 | "start": "cross-env NODE_ENV=production babel-node ./Server.js", 23 | "build": "npm run build:css && npm run build:js", 24 | "build:css": "lessc less/app.less public/css/app.css && cleancss public/css/app.css -o public/css/app.css", 25 | "build:js": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js", 26 | "test": "cross-env NODE_ENV=test mocha -R spec --require @babel/register --require ./tests/testHelper.js tests/**/*.js", 27 | "test:watch": "npm test -- --watch", 28 | "coverage": "cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text mocha tests/**/*.js", 29 | "codecov": "cat ./coverage/lcov.info | codecov" 30 | }, 31 | "husky": { 32 | "hooks": { 33 | "pre-commit": "lint-staged && npm run test" 34 | } 35 | }, 36 | "lint-staged": { 37 | "src/**/*.{js,jsx}": [ 38 | "eslint" 39 | ] 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/chejen/keys-translations-manager" 44 | }, 45 | "engines": { 46 | "node": ">=10" 47 | }, 48 | "dependencies": { 49 | "@babel/node": "^7.10.5", 50 | "@babel/plugin-proposal-class-properties": "^7.10.4", 51 | "@babel/preset-env": "^7.11.5", 52 | "@babel/preset-react": "^7.10.4", 53 | "archiver": "^5.0.2", 54 | "body-parser": "^1.19.0", 55 | "clean-webpack-plugin": "^3.0.0", 56 | "compression": "^1.7.4", 57 | "cors": "^2.8.5", 58 | "cross-env": "^7.0.2", 59 | "d3-hierarchy": "^1.1.9", 60 | "d3-selection": "^1.4.1", 61 | "d3-zoom": "^1.8.3", 62 | "ejs": "^3.1.5", 63 | "es6-promise": "^4.2.8", 64 | "express": "^4.17.1", 65 | "history": "^4.10.1", 66 | "isomorphic-fetch": "^3.0.0", 67 | "keys-translations-manager-core": "^1.6.0", 68 | "lodash": "^4.17.20", 69 | "mongoose": "^5.10.7", 70 | "multiparty": "^4.2.2", 71 | "nodemon": "^2.0.4", 72 | "prop-types": "~15.7.2", 73 | "react": "~16.13.1", 74 | "react-bootstrap": "~0.32.4", 75 | "react-dom": "~16.13.1", 76 | "react-dropzone": "^10.2.1", 77 | "react-hot-loader": "^4.13.0", 78 | "react-redux": "^7.2.1", 79 | "react-router": "^5.2.0", 80 | "react-router-dom": "^5.2.0", 81 | "react-table": "^6.10.3", 82 | "redux": "^4.0.5", 83 | "redux-thunk": "^2.3.0", 84 | "serve-favicon": "^2.5.0", 85 | "socket.io": "^2.3.0", 86 | "socket.io-client": "^2.3.0" 87 | }, 88 | "devDependencies": { 89 | "@babel/core": "^7.11.6", 90 | "@babel/register": "^7.11.5", 91 | "@hot-loader/react-dom": "^16.11.0", 92 | "@istanbuljs/nyc-config-babel": "^3.0.0", 93 | "babel-eslint": "^10.1.0", 94 | "babel-loader": "^8.1.0", 95 | "babel-plugin-add-module-exports": "^1.0.4", 96 | "babel-plugin-istanbul": "^6.0.0", 97 | "chai": "^4.2.0", 98 | "clean-css-cli": "^4.3.0", 99 | "codecov": "^3.7.2", 100 | "css-loader": "^4.3.0", 101 | "enzyme": "^3.11.0", 102 | "enzyme-adapter-react-16": "^1.15.5", 103 | "eslint": "^7.10.0", 104 | "eslint-loader": "^4.0.2", 105 | "eslint-plugin-babel": "^5.3.1", 106 | "eslint-plugin-i18n": "^2.0.0", 107 | "eslint-plugin-import": "^2.22.1", 108 | "eslint-plugin-react": "^7.21.2", 109 | "eventsource-polyfill": "~0.9.6", 110 | "husky": "^4.3.0", 111 | "jsdom": "^16.4.0", 112 | "less": "^3.12.2", 113 | "less-loader": "^7.0.1", 114 | "lint-staged": "^10.4.0", 115 | "mocha": "^8.1.3", 116 | "nock": "^13.0.4", 117 | "nyc": "^15.1.0", 118 | "react-test-renderer": "~16.13.0", 119 | "redux-mock-store": "^1.5.4", 120 | "rename": "^1.0.4", 121 | "sinon": "^9.0.3", 122 | "sinon-chai": "^3.5.0", 123 | "style-loader": "1.2.1", 124 | "webpack": "^4.44.2", 125 | "webpack-cli": "^3.3.12", 126 | "webpack-dev-middleware": "^3.7.2", 127 | "webpack-hot-middleware": "^2.25.0", 128 | "webpack-strip": "~0.1.0" 129 | }, 130 | "license": "MIT" 131 | } 132 | -------------------------------------------------------------------------------- /packages/keys-translations-manager-cli/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | node_modules 4 | -------------------------------------------------------------------------------- /packages/keys-translations-manager-cli/README.md: -------------------------------------------------------------------------------- 1 | [![NPM License][npm-license-image]][npm-license-url] 2 | [![NPM Version][npm-version-image]][npm-version-url] 3 | 4 | [npm-license-image]: https://img.shields.io/npm/l/keys-translations-manager-cli.svg 5 | [npm-license-url]: https://www.npmjs.com/package/keys-translations-manager-cli 6 | [npm-version-image]: https://img.shields.io/npm/v/keys-translations-manager-cli.svg 7 | [npm-version-url]: https://www.npmjs.com/package/keys-translations-manager-cli 8 | 9 | # keys-translations-manager-cli 10 | > It's a cli tool that helps you download locales managed by [keys-translations-manager](https://github.com/chejen/keys-translations-manager). 11 | 12 | * Older version: [v1.0.0](https://github.com/chejen/keys-translations-manager/blob/master/docs/cli-v1.0.md) 13 | 14 | 15 | ## Installation 16 | Global installation: 17 | ```sh 18 | $ npm install -g keys-translations-manager-cli 19 | ``` 20 | 21 | Local installation: 22 | ```sh 23 | $ npm install --save-dev keys-translations-manager-cli 24 | ``` 25 | 26 | ## Configuration 27 | Add `.ktmrc` to your home directory (or add `.ktmrc` into your project if you installed the cli tool locally.) 28 | 29 | * Sample `.ktmrc`: 30 | ```json 31 | { 32 | "database": "mongodb://localhost:27017/translationdb", 33 | "outputs": [{ 34 | "project": "p1", 35 | "locales": ["en-US", "zh-TW"], 36 | "type": "json", 37 | "filename": "${locale}", 38 | "path": "/path/to/project1", 39 | "formatted": true 40 | }, { 41 | "project": "p2", 42 | "locales": ["en-US", "zh-TW"], 43 | "type": "properties", 44 | "filename": "translation", 45 | "path": "/path/to/project2/${locale}" 46 | }] 47 | } 48 | ``` 49 | 50 | | Properties | Description | Required | 51 | |:----------:|:-----|:-----:| 52 | | project | Specify a project ID set in [ktm.config.js](https://github.com/chejen/keys-translations-manager/blob/master/ktm.config.js)| Y | 53 | | locales | Specify locales to output.| Y | 54 | | type | Specify one of the following: `json` (nested JSON), `flat` (flat JSON) or `properties`. | Y | 55 | | filename | Specify a name for output file. | Y | 56 | | path | Specify an output path. | Y | 57 | | formatted | Sort keys alphabetically. | 58 | 59 | - `${locale}` can be a placeholder for **filename** and/or **path**. 60 | 61 | 62 | ## Usage 63 | ``` 64 | ktm 65 | ``` 66 | can be one of the following: 67 | * `export`: Export locales to specified paths. 68 | * `reset`: Drop the database used in KTM. 69 | 70 | 71 | ## Example 72 | If you globally installed the cli tool, execute the command like this: 73 | ```sh 74 | $ ktm export 75 | ``` 76 | Or, if you had it installed locally by your project, you can add `ktm` script to package.json's **scripts** property, 77 | ```js 78 | "scripts": { 79 | "ktm": "ktm export" 80 | } 81 | ``` 82 | then execute: 83 | ```sh 84 | $ npm run ktm 85 | ``` 86 | 87 | Finally, you will get your outputs like these: 88 | * /path/to/project1/en-US.json 89 | * /path/to/project1/zh-TW.json 90 | * /path/to/project2/en-US/translation.properties 91 | * /path/to/project2/zh-TW/translation.properties 92 | -------------------------------------------------------------------------------- /packages/keys-translations-manager-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keys-translations-manager-cli", 3 | "version": "1.6.0", 4 | "description": "keys-translations-manager's command line.", 5 | "keywords": [ 6 | "i18n", 7 | "internationalization", 8 | "lang", 9 | "language", 10 | "l10n", 11 | "locale", 12 | "localization", 13 | "multilingual", 14 | "translation", 15 | "MERN" 16 | ], 17 | "author": "chejen", 18 | "license": "MIT", 19 | "repository": "https://github.com/chejen/keys-translations-manager/tree/master/packages/keys-translations-manager-cli", 20 | "dependencies": { 21 | "keys-translations-manager-core": "^1.6.0", 22 | "inquirer": "^6.2.2", 23 | "yargs": "^13.2.2", 24 | "node-fs": "0.1.7", 25 | "mongoose": "^5.4.21", 26 | "snyk": "^1.192.3" 27 | }, 28 | "bin": { 29 | "ktm": "./bin/ktm.js" 30 | }, 31 | "scripts": { 32 | "snyk-protect": "snyk protect", 33 | "prepublish": "npm run snyk-protect" 34 | }, 35 | "snyk": true 36 | } 37 | -------------------------------------------------------------------------------- /packages/keys-translations-manager-core/README.md: -------------------------------------------------------------------------------- 1 | # keys-translations-manager-core 2 | > [keys-translations-manager](https://github.com/chejen/keys-translations-manager)'s core 3 | -------------------------------------------------------------------------------- /packages/keys-translations-manager-core/index.js: -------------------------------------------------------------------------------- 1 | exports.historyUtil = require('./lib/historyUtil'); 2 | exports.importUtil = require('./lib/importUtil'); 3 | exports.localeUtil = require('./lib/localeUtil'); 4 | exports.logUtil = require('./lib/logUtil'); 5 | exports.mergeUtil = require('./lib/mergeUtil'); 6 | exports.timingUtil = require('./lib/timingUtil'); 7 | exports.transformationUtil = require('./lib/transformationUtil'); 8 | -------------------------------------------------------------------------------- /packages/keys-translations-manager-core/lib/historyUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | /** 5 | * result = { 6 | * key: { 7 | * // case 1: new value added 8 | * original: undefined, 9 | * addition: 'ui.common.edit', 10 | * deletion: undefined, 11 | * 12 | * // case 2: no difference 13 | * // original: '', // or 14 | * original: 'ui.common.edit', 15 | * addition: undefined, 16 | * deletion: undefined, 17 | * 18 | * // case 3: value modified 19 | * original: undefined, 20 | * addition: 'ui.common.modify', 21 | * deletion: 'ui.common.edit', 22 | * 23 | * // case 4: value removed 24 | * original: undefined, 25 | * addition: undefined, 26 | * deletion: 'ui.common.modify', 27 | * } 28 | * } 29 | */ 30 | differentiate: function(prevLog, log) { 31 | var result = {}, 32 | key, 33 | prevValue, 34 | value; 35 | 36 | if (!log) { 37 | return false; 38 | } 39 | 40 | if (prevLog) { 41 | for (key in prevLog) { 42 | if (prevLog.hasOwnProperty(key)) { 43 | prevValue = prevLog[key]; 44 | if (typeof result[key] === 'undefined') { 45 | result[key] = {}; 46 | } 47 | if (log && log.hasOwnProperty(key)) { 48 | value = log[key]; 49 | if (typeof prevValue !== 'object') { // string, number, ... 50 | if (prevValue === value) { 51 | result[key].original = prevValue; 52 | } else { 53 | result[key].deletion = prevValue; 54 | result[key].addition = value; 55 | } 56 | } else { // 'project' is an array 57 | // It doesn't matter that the arrays are mutated 58 | if (prevValue.sort().join(',') === value.sort().join(',')) { 59 | result[key].original = prevValue; 60 | } else { 61 | result[key].deletion = prevValue; 62 | result[key].addition = value; 63 | } 64 | } 65 | } else { 66 | result[key].deletion = prevValue; 67 | } 68 | } 69 | } 70 | } 71 | 72 | for (key in log) { 73 | if (log.hasOwnProperty(key)) { 74 | if (!prevLog || !prevLog.hasOwnProperty(key)) { 75 | if (typeof result[key] === 'undefined') { 76 | result[key] = {}; 77 | } 78 | result[key].addition = log[key]; 79 | } 80 | } 81 | } 82 | 83 | return result; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/keys-translations-manager-core/lib/importUtil.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var propertiesParser = require("properties-parser"); 3 | 4 | module.exports = { 5 | read: function(filename, callback) { 6 | fs.readFile(filename, {encoding: 'utf-8'}, function(err, data){ 7 | if (err) { 8 | callback(err); 9 | } else { 10 | // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) 11 | if (data.charCodeAt(0) === 0xFEFF) { 12 | data = data.slice(1); 13 | } 14 | 15 | if (filename.search(/\.json$/i) >= 0) { 16 | data = JSON.parse(data); 17 | callback(null, 'json', data); 18 | 19 | } else if (filename.search(/\.properties$/i) >= 0) { 20 | propertiesParser.read(filename, function(err, data){ 21 | callback(err, 'properties', data); 22 | }) 23 | 24 | } else { 25 | try { 26 | data = JSON.parse(data); 27 | callback(null, 'json', data); 28 | } catch(e) { 29 | propertiesParser.read(filename, function(err, data){ 30 | callback(err, 'properties', data); 31 | }) 32 | } 33 | } 34 | } 35 | }); 36 | }, 37 | 38 | validate: function(srcData, destData) {// srcData(from file); destData(from db) 39 | var prefix = "$$", 40 | lenPrefix = prefix.length, 41 | key, 42 | i, 43 | tmpKey, 44 | segment, 45 | lenSegment, 46 | lenDestData = destData.length, 47 | data, 48 | srcKey, 49 | destKey, 50 | type, 51 | srcHash = {}, 52 | destHash = {}, 53 | error = { 54 | "iequals": [], 55 | "iconflicts": [] 56 | }; 57 | 58 | // srcData processing 59 | for (key in srcData) { 60 | tmpKey = ""; 61 | segment = key.split("."); 62 | lenSegment = segment.length; 63 | 64 | for (i=0; i < lenSegment; i++) { 65 | if (i === lenSegment - 1) { 66 | srcHash[key] = [key]; 67 | } else { 68 | tmpKey += (i ? "." : "") + segment[i]; 69 | srcHash[prefix + tmpKey] = [prefix + key]; 70 | } 71 | } 72 | } 73 | 74 | // destData processing 75 | while(lenDestData--){ 76 | tmpKey = ""; 77 | data = destData[lenDestData]; 78 | key = data.key; 79 | segment = key.split("."); 80 | lenSegment = segment.length; 81 | 82 | for (i=0; i < lenSegment; i++) { 83 | if (i === lenSegment - 1) { 84 | destHash[key] = [key]; 85 | } else { 86 | tmpKey += (i ? "." : "") + segment[i]; 87 | destHash[tmpKey] = [prefix + key]; 88 | } 89 | } 90 | } 91 | 92 | 93 | // check if keys conflict 94 | for (key in destHash) { 95 | destKey = destHash[key][0]; 96 | 97 | srcKey = srcHash[key]; 98 | if (srcKey) { 99 | if (destKey.indexOf(prefix) === 0) { 100 | type = "iconflicts"; //"ibelongsTo" 101 | } else { 102 | type = "iequals"; 103 | } 104 | error[type].push( srcKey[0] ); 105 | } 106 | 107 | srcKey = srcHash[prefix + key]; 108 | if (srcKey) { 109 | if (destKey.indexOf(prefix) === 0) { 110 | continue; 111 | } else { 112 | type = "iconflicts"; //"icontains" 113 | } 114 | error[type].push( srcKey[0].substr(lenPrefix) ); 115 | } 116 | } 117 | 118 | return error; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /packages/keys-translations-manager-core/lib/localeUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | setMessages: function(messages) { 4 | this.messages = messages 5 | }, 6 | getMsg: function(path) { 7 | var pathParts = path ? path.split('.') : [""], 8 | len = arguments.length - 1, 9 | message, i; 10 | 11 | try { 12 | message = pathParts.reduce(function (obj, pathPart) { 13 | return obj[pathPart]; 14 | }, this.messages); 15 | } catch (e) { 16 | //console.error(e); 17 | message = ""; 18 | } finally { 19 | if (message) { 20 | for (i = 0; i < len;){ 21 | message = message.replace( 22 | new RegExp("\\{" + i + "\\}", "gm"), 23 | arguments[++i] 24 | ); 25 | } 26 | } else { 27 | message = path + ".undefined"; 28 | } 29 | } 30 | 31 | return message; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/keys-translations-manager-core/lib/logUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chalk = require('chalk'); 3 | module.exports = { 4 | log: function(level, msg){ 5 | var tag; 6 | switch (level) { 7 | case 'info': 8 | tag = chalk.bold.green(" [INFO] "); 9 | break; 10 | /* istanbul ignore next */ 11 | case 'warn': 12 | tag = chalk.bold.yellow(" [WARN] "); 13 | break; 14 | /* istanbul ignore next */ 15 | case 'error': 16 | tag = chalk.bold.red(" [ERROR] "); 17 | break; 18 | /* istanbul ignore next */ 19 | default: 20 | tag = " "; 21 | break; 22 | } 23 | console.log(chalk.grey(" ktm") + tag + msg); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /packages/keys-translations-manager-core/lib/mergeUtil.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | findMergeable: function(translations, locales) { 3 | var translation, 4 | l, 5 | lenLocales, 6 | keyHash = {}, 7 | keyCollision, 8 | translationHash, 9 | translationCollision, 10 | translationSet, 11 | keys = {}, 12 | mergeable = []; 13 | 14 | if (!translations || !locales) { 15 | return { 16 | keys: {}, 17 | mergeable: [] 18 | }; 19 | } 20 | 21 | l = translations.length; 22 | lenLocales = locales.length; 23 | 24 | while(l--){ 25 | translation = translations[l]; 26 | keyCollision = keyHash[translation.key]; 27 | if (keyCollision) { 28 | keyCollision.push(translation); 29 | } else { 30 | keyHash[translation.key] = [translation]; 31 | } 32 | } 33 | 34 | for (var key in keyHash) { 35 | keyCollision = keyHash[key]; 36 | if (keyCollision.length >= 2) { 37 | translationHash = {}; 38 | for (var j=0, kc; j < keyCollision.length; j++) { 39 | translationSet = ""; 40 | kc = keyCollision[j]; 41 | 42 | for (var i=0; i < lenLocales; i++) { 43 | translationSet += (kc[ locales[i] ] ? kc[ locales[i] ] : ""); 44 | } 45 | 46 | translationCollision = translationHash[translationSet]; 47 | if (translationCollision) { 48 | translationCollision.push(kc); 49 | } else { 50 | translationHash[translationSet] = [kc]; 51 | } 52 | } 53 | 54 | for (var innerKey in translationHash) { 55 | if (translationHash[innerKey].length >= 2) { 56 | keys[key] = true; 57 | mergeable.push(translationHash[innerKey]); 58 | } 59 | } 60 | } 61 | } 62 | 63 | return { 64 | keys: keys, 65 | mergeable: mergeable 66 | }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/keys-translations-manager-core/lib/timingUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | setTimeoutId: function(timeoutId) { 4 | this.timeoutId = timeoutId 5 | }, 6 | getTimeoutId: function() { 7 | return this.timeoutId; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/keys-translations-manager-core/lib/transformationUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var transformationUtil = { 3 | properties2Json: function(jsonObj, propertyKey, propertyValue) { 4 | var childObj = {}, 5 | keyPart, 6 | i, 7 | keyParts = propertyKey.split("."), 8 | lenParts = keyParts.length; 9 | 10 | for (i = 0; i < lenParts; i++) { 11 | keyPart = keyParts[i]; 12 | 13 | if (i === lenParts - 1) { 14 | if (i === 0) { 15 | jsonObj[keyPart] = propertyValue; 16 | } else { 17 | childObj[keyPart] = propertyValue; 18 | } 19 | break; 20 | } 21 | 22 | if (i === 0) { 23 | if (!jsonObj[keyPart]) { 24 | jsonObj[keyPart] = {}; 25 | } 26 | childObj = jsonObj[keyPart]; 27 | } else { 28 | if (!childObj[keyPart]) { 29 | childObj[keyPart] = {}; 30 | } 31 | childObj = childObj[keyPart]; 32 | } 33 | } 34 | 35 | return jsonObj; 36 | }, 37 | 38 | json2Properties: function(properties, jsonObj, initStr) { 39 | var key, 40 | newKey; 41 | 42 | for (key in jsonObj) { 43 | newKey = initStr ? initStr + "." + key : key; 44 | if (typeof jsonObj[key] === "object") { 45 | properties = transformationUtil.json2Properties(properties, jsonObj[key], newKey); 46 | } else { 47 | properties[newKey] = jsonObj[key]; 48 | } 49 | } 50 | return properties; 51 | }, 52 | 53 | json2Tree: function(jsonObj) { 54 | var ary = []; 55 | for (var key in jsonObj) { 56 | if (jsonObj[key]._id) { 57 | ary.push({ 58 | "name": key, 59 | "translations": jsonObj[key] 60 | }); 61 | } else { 62 | ary.push({ 63 | "name": key, 64 | "children": transformationUtil.json2Tree(jsonObj[key]) 65 | }); 66 | } 67 | } 68 | return ary; 69 | }, 70 | 71 | document2FileContent: function(translations, locale, fileType, formatted) { 72 | var len = translations.length, 73 | translation, 74 | rootObj = {}, 75 | str = "", 76 | formatContent = function(obj) { 77 | if (formatted === true) { //formatted 78 | return JSON.stringify(obj, null, 2); 79 | } else { //minimized 80 | return JSON.stringify(obj); 81 | } 82 | }; 83 | 84 | if (fileType === "json") { //nested JSON 85 | while(len--) { 86 | translation = translations[len]; 87 | rootObj = transformationUtil.properties2Json(rootObj, translation.key, translation[locale]); 88 | } 89 | 90 | str = formatContent(rootObj); 91 | 92 | } else if (fileType === "flat") { //flat JSON 93 | while(len--) { 94 | translation = translations[len]; 95 | rootObj[translation.key] = translation[locale]; 96 | } 97 | 98 | str = formatContent(rootObj); 99 | 100 | } else if (fileType === "properties") { 101 | while(len--) { 102 | translation = translations[len]; 103 | str += translation.key + "=" + translation[locale] + "\r\n"; 104 | } 105 | } 106 | 107 | return str; 108 | } 109 | }; 110 | 111 | module.exports = transformationUtil; 112 | -------------------------------------------------------------------------------- /packages/keys-translations-manager-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keys-translations-manager-core", 3 | "version": "1.6.0", 4 | "description": "keys-translations-manager's core", 5 | "keywords": [ 6 | "i18n", 7 | "internationalization", 8 | "lang", 9 | "language", 10 | "l10n", 11 | "locale", 12 | "localization", 13 | "multilingual", 14 | "translation", 15 | "MERN" 16 | ], 17 | "author": "chejen", 18 | "license": "MIT", 19 | "repository": "https://github.com/chejen/keys-translations-manager/tree/master/packages/keys-translations-manager-core", 20 | "main": "./index.js", 21 | "dependencies": { 22 | "chalk": "^2.4.1", 23 | "properties-parser": "0.3.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/image/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chejen/keys-translations-manager/ce80c280b99092614cac193c6d65290e5522fb15/public/image/favicon.ico -------------------------------------------------------------------------------- /public/locale/en-US/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "ui": { 3 | "common": { 4 | "action": "Actions", 5 | "add": "Add", 6 | "applyto": "Apply to", 7 | "cancel": "Cancel", 8 | "close": "Close", 9 | "delete": "Delete", 10 | "desc": "Description", 11 | "download": "Download", 12 | "edit": "Edit", 13 | "file": "File", 14 | "goBack": "Go back", 15 | "history": "History", 16 | "import": "Import", 17 | "language": "Language", 18 | "locale": "Locale", 19 | "locales": "Locales", 20 | "merge": "Merge", 21 | "or": "or", 22 | "others": "and {0} others", 23 | "reload": "Reload", 24 | "reset": "Reset", 25 | "update": "Update" 26 | }, 27 | "confirm": { 28 | "continue": "Are you sure you want to continue?", 29 | "delete": "Are you sure you want to delete \"{0}\" ?", 30 | "header": "Confirm", 31 | "no": "No", 32 | "yes": "Yes" 33 | }, 34 | "err": { 35 | "belongsTo": "【JSON::ParserError】\"{0}\" conflicts with {1}* in the following project(s): {2}", 36 | "contains": "【JSON::ParserError】\"{0}\" conflicts with \"{1}\" in the following project(s): {2}", 37 | "emptyfield": "The following field(s) are required: {0}", 38 | "equals": "\"{0}\" already exists in the following project(s): {1}", 39 | "iconflicts": "The following key(s) will cause JSON-parsing error(s):", 40 | "iequals": "The following key(s) will override original translation(s):" 41 | }, 42 | "file": { 43 | "accept": "Accept {0} only.", 44 | "select": "Drop your locale file here, or click to select a file to import.", 45 | "selected": "The file to import:" 46 | }, 47 | "grid": { 48 | "edit": "Double-click the cell to edit. Press Enter to commit.", 49 | "empty": "No data", 50 | "search": "Search" 51 | }, 52 | "history": { 53 | "add": "Added", 54 | "at": "at", 55 | "delete": "Deleted", 56 | "edit": "Edited", 57 | "import": "Imported", 58 | "merge": "Merged", 59 | "none": "No translation history found." 60 | }, 61 | "json": { 62 | "format": "formatted", 63 | "mini": "minimized" 64 | }, 65 | "merge": { 66 | "match": "The following key(s) will be merged:", 67 | "nomatch": "No keys need to be merged." 68 | }, 69 | "pagination": { 70 | "next": "Next", 71 | "of": "of", 72 | "page": "Page", 73 | "previous": "Previous", 74 | "rows": "rows" 75 | }, 76 | "tip": { 77 | "dataChanged": "Data has been changed by others.", 78 | "iconflicts": "(e.g.) \"ui.common.add\" will conflict with \"ui\", \"ui.common\", and/or \"ui.common.add.*\" while parsing to JSON format.", 79 | "iequals": "This means you already have translations with these keys for selected locale and project." 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /public/locale/zh-CN/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "ui": { 3 | "common": { 4 | "action": "动作", 5 | "add": "新增", 6 | "applyto": "套用至", 7 | "cancel": "取消", 8 | "close": "关闭", 9 | "delete": "删除", 10 | "desc": "描述", 11 | "download": "下载", 12 | "edit": "编辑", 13 | "file": "档案", 14 | "goBack": "返回", 15 | "history": "检视历史记录", 16 | "import": "导入", 17 | "language": "语言", 18 | "locale": "语言环境", 19 | "locales": "语言环境", 20 | "merge": "合并", 21 | "or": "或", 22 | "others": "和其他 {0} 个", 23 | "reload": "重载", 24 | "reset": "还原", 25 | "update": "更新" 26 | }, 27 | "confirm": { 28 | "continue": "您确定要继续吗 ?", 29 | "delete": "您确定要删除 \"{0}\" 吗?", 30 | "header": "确认", 31 | "no": "否", 32 | "yes": "是" 33 | }, 34 | "err": { 35 | "belongsTo": "【JSON::ParserError】\"{0}\" 与 {1}* 在 {2} 冲突", 36 | "contains": "【JSON::ParserError】\"{0}\" 与 \"{1}\" 在 {2} 冲突", 37 | "emptyfield": "以下字段为必填: {0}", 38 | "equals": "\"{0}\" 已经存在于以下项目: {1}", 39 | "iconflicts": "以下这些 key 会在解析 JSON 时发生错误:", 40 | "iequals": "以下这些 key 会覆盖掉已经存在的翻译:" 41 | }, 42 | "file": { 43 | "accept": "仅接受 {0} 的文件格式。", 44 | "select": "请将您的语言环境档拖曳至此,或点击此区以选取要导入的档案。", 45 | "selected": "您选取的档案:" 46 | }, 47 | "grid": { 48 | "edit": "双击单元格可进行编辑;键入 Enter 以送出更新", 49 | "empty": "无数据", 50 | "search": "关键词搜寻" 51 | }, 52 | "history": { 53 | "add": "新增", 54 | "at": "于", 55 | "delete": "删除", 56 | "edit": "编辑", 57 | "import": "汇入", 58 | "merge": "合并", 59 | "none": "无此翻译的历史记录。" 60 | }, 61 | "json": { 62 | "format": "格式化", 63 | "mini": "最小化" 64 | }, 65 | "merge": { 66 | "match": "以下这些 key 将会被合并:", 67 | "nomatch": "无符合合并条件的数据" 68 | }, 69 | "pagination": { 70 | "next": "下一页", 71 | "of": "/", 72 | "page": "页", 73 | "previous": "上一页", 74 | "rows": "笔" 75 | }, 76 | "tip": { 77 | "dataChanged": "数据已经被其他人异动过", 78 | "iconflicts": "(例) 在解析成 JSON 格式时,ui.common.add 会与 ui、ui.common、ui.common.add.* 等 key 发生冲突。", 79 | "iequals": "出现这个错误表示,这些 key 在您选取的项目中已经存在,而且选取的语言环境也已经有对应的翻译。" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /public/locale/zh-TW/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "ui": { 3 | "common": { 4 | "action": "動作", 5 | "add": "新增", 6 | "applyto": "套用至", 7 | "cancel": "取消", 8 | "close": "關閉", 9 | "delete": "刪除", 10 | "desc": "描述", 11 | "download": "下載", 12 | "edit": "編輯", 13 | "file": "檔案", 14 | "goBack": "返回", 15 | "history": "檢視歷史記錄", 16 | "import": "匯入", 17 | "language": "語言", 18 | "locale": "語系", 19 | "locales": "語系", 20 | "merge": "合併", 21 | "or": "或", 22 | "others": "和其他 {0} 個", 23 | "reload": "重新載入", 24 | "reset": "還原", 25 | "update": "更新" 26 | }, 27 | "confirm": { 28 | "continue": "您確定要繼續嗎 ?", 29 | "delete": "您確定要刪除 \"{0}\" 嗎?", 30 | "header": "確認", 31 | "no": "否", 32 | "yes": "是" 33 | }, 34 | "err": { 35 | "belongsTo": "【JSON::ParserError】\"{0}\" 與 {1}* 在 {2} 衝突", 36 | "contains": "【JSON::ParserError】\"{0}\" 與 \"{1}\" 在 {2} 衝突", 37 | "emptyfield": "以下欄位為必填: {0}", 38 | "equals": "\"{0}\" 已經存在於以下專案: {1}", 39 | "iconflicts": "以下這些 key 會在解析 JSON 時發生錯誤:", 40 | "iequals": "以下這些 key 會覆蓋掉已經存在的翻譯:" 41 | }, 42 | "file": { 43 | "accept": "僅接受 {0} 的檔案格式。", 44 | "select": "請將您的語系檔拖曳至此,或點擊此區以選取要匯入的檔案。", 45 | "selected": "您選取的檔案:" 46 | }, 47 | "grid": { 48 | "edit": "雙擊儲存格可進行編輯;鍵入 Enter 以送出更新", 49 | "empty": "無資料", 50 | "search": "關鍵字搜尋" 51 | }, 52 | "history": { 53 | "add": "新增", 54 | "at": "於", 55 | "delete": "刪除", 56 | "edit": "編輯", 57 | "import": "匯入", 58 | "merge": "合併", 59 | "none": "無此翻譯的歷史記錄。" 60 | }, 61 | "json": { 62 | "format": "格式化", 63 | "mini": "最小化" 64 | }, 65 | "merge": { 66 | "match": "以下這些 key 將會被合併:", 67 | "nomatch": "無符合合併條件的資料" 68 | }, 69 | "pagination": { 70 | "next": "下一頁", 71 | "of": "/", 72 | "page": "頁", 73 | "previous": "上一頁", 74 | "rows": "筆" 75 | }, 76 | "tip": { 77 | "dataChanged": "資料已經被其他人異動過", 78 | "iconflicts": "(例) 在解析成 JSON 格式時,ui.common.add 會與 ui、ui.common、ui.common.add.* 等 key 發生衝突。", 79 | "iequals": "出現這個錯誤表示,這些 key 在您選取的專案中已經存在,而且選取的語系也已經有對應的翻譯。" 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/actions/components.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes' 2 | 3 | export function showEditModal(record) { 4 | return { 5 | type: ActionTypes.SHOW_EDITMODAL, 6 | record: record 7 | } 8 | } 9 | 10 | export function closeEditModal() { 11 | return { 12 | type: ActionTypes.CLOSE_EDITMODAL 13 | } 14 | } 15 | 16 | export function showConfirmModal(record) { 17 | return { 18 | type: ActionTypes.SHOW_CONFIRMMODAL, 19 | record, 20 | } 21 | } 22 | 23 | export function closeConfirmModal() { 24 | return { 25 | type: ActionTypes.CLOSE_CONFIRMMODAL 26 | } 27 | } 28 | 29 | export function showHistoryModal(record) { 30 | return { 31 | type: ActionTypes.SHOW_HISTORYMODAL, 32 | record, 33 | } 34 | } 35 | 36 | export function closeHistoryModal() { 37 | return { 38 | type: ActionTypes.CLOSE_HISTORYMODAL 39 | } 40 | } 41 | 42 | export function closeMergeModal() { 43 | return { 44 | type: ActionTypes.CLOSE_MERGEMODAL 45 | } 46 | } 47 | 48 | export function showImportModal() { 49 | return { 50 | type: ActionTypes.SHOW_IMPORTMODAL 51 | } 52 | } 53 | 54 | export function closeImportModal() { 55 | return { 56 | type: ActionTypes.CLOSE_IMPORTMODAL 57 | } 58 | } 59 | 60 | export function showMessagePopup() { 61 | return { 62 | type: ActionTypes.SHOW_MESSAGEPOPUP 63 | } 64 | } 65 | 66 | export function closeMessagePopup() { 67 | return { 68 | type: ActionTypes.CLOSE_MESSAGEPOPUP 69 | } 70 | } 71 | 72 | export function reloadData() { 73 | return { 74 | type: ActionTypes.RELOAD_DATA 75 | } 76 | } 77 | 78 | export function showTooltip(top, left) { 79 | return { 80 | type: ActionTypes.SHOW_TOOLTIP, 81 | top, 82 | left 83 | } 84 | } 85 | 86 | export function hideTooltip() { 87 | return { 88 | type: ActionTypes.HIDE_TOOLTIP 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/actions/counts.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes' 2 | import configUtil from '../configUtil' 3 | 4 | export function loadCounts() { 5 | return dispatch => { 6 | return fetch(configUtil.getHost() + '/api/count/projects?t=' + +new Date()) 7 | .then(res => { 8 | if (res.status >= 400) { 9 | throw new Error(res.status + ", " + res.statusText); 10 | } 11 | return res.json(); 12 | }) 13 | .then((result) => { 14 | let l = result.length, 15 | field = "_id", 16 | c, 17 | o = {}; 18 | 19 | while(l--){ 20 | c = result[l]; 21 | o[c[field]] = c.count; 22 | } 23 | dispatch({ 24 | type: ActionTypes.LOAD_COUNTS, 25 | counts: o 26 | }) 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/actions/errors.js: -------------------------------------------------------------------------------- 1 | import { ALERT_ERRORS, CLEAR_ERRORS } from '../constants/ActionTypes' 2 | 3 | export function alertErrors(errors) { 4 | return { 5 | type: ALERT_ERRORS, 6 | errors: errors 7 | } 8 | } 9 | 10 | export function clearErrors() { 11 | return { 12 | type: CLEAR_ERRORS, 13 | errors: [] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/actions/history.js: -------------------------------------------------------------------------------- 1 | import { LOAD_HISTORY } from '../constants/ActionTypes' 2 | import * as Status from '../constants/Status' 3 | import configUtil from '../configUtil' 4 | 5 | const setHistory = (status, historylog) => ({ 6 | type: LOAD_HISTORY, 7 | status, 8 | historylog, 9 | }); 10 | 11 | export function loadHistory(translationId) { 12 | return dispatch => { 13 | dispatch(setHistory(Status.STATUS_FETCHING, [])); 14 | return fetch(configUtil.getHost() + `/api/history/${translationId}?t=` + +new Date()) 15 | .then(res => { 16 | if (res.status >= 400) { 17 | dispatch(setHistory(Status.STATUS_ERROR, [])); 18 | throw new Error(res.status + ", " + res.statusText); 19 | } 20 | return res.json(); 21 | }) 22 | .then(result => { 23 | dispatch(setHistory(Status.STATUS_FETCHED, result ? result.logs : [])); 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/actions/keys.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes' 2 | import configUtil from '../configUtil' 3 | 4 | export function findMergeable() { 5 | return dispatch => { 6 | return fetch(configUtil.getHost() + '/api/key?t=' + +new Date()) 7 | .then(res => { 8 | if (res.status >= 400) { 9 | throw new Error(res.status + ", " + res.statusText); 10 | } 11 | return res.json(); 12 | }) 13 | .then((result) => { 14 | dispatch({ 15 | type: ActionTypes.FIND_MERGEABLE, 16 | data: result 17 | }) 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/actions/messages.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes' 2 | import configUtil from '../configUtil' 3 | 4 | export function loadMessages(lang) { 5 | return dispatch => { 6 | return fetch(configUtil.getHost() + '/public/locale/' + lang + '/translation.json') 7 | .then(res => { 8 | if (res.status >= 400) { 9 | throw new Error(res.status + ", " + res.statusText); 10 | } 11 | return res.json(); 12 | }) 13 | .then((messages) => { 14 | dispatch({ 15 | type: ActionTypes.LOAD_MESSAGES, 16 | lang, 17 | messages 18 | }) 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/actions/socket.js: -------------------------------------------------------------------------------- 1 | import { END_DATACHANGE } from '../constants/ActionTypes' 2 | 3 | export function endDataChange() { 4 | return { 5 | type: END_DATACHANGE 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/actions/translations.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes' 2 | import configUtil from '../configUtil' 3 | 4 | export function addTranslation(params) { 5 | return dispatch => { 6 | return fetch(configUtil.getHost() + '/api/translation', { 7 | headers: { 8 | 'Accept': 'application/json; charset=utf-8', 9 | 'Content-Type': 'application/json; charset=utf-8' 10 | }, 11 | method: 'POST', 12 | body: JSON.stringify(params) 13 | }) 14 | .then(res => { 15 | if (res.status >= 400) { 16 | throw new Error(res.status + ", " + res.statusText); 17 | } 18 | return res.json(); 19 | }) 20 | .then((result) => { 21 | if (result.success) { 22 | dispatch({ 23 | type: ActionTypes.ADD_TRANSLATION, 24 | data: result.data 25 | }) 26 | } else { 27 | dispatch({ 28 | type: ActionTypes.ALERT_ERRORS, 29 | errors: result.errors 30 | }) 31 | } 32 | }) 33 | } 34 | } 35 | 36 | export function loadTranslations() { 37 | return dispatch => { 38 | return fetch(configUtil.getHost() + '/api/translation?t=' + +new Date()) 39 | .then(res => { 40 | if (res.status >= 400) { 41 | throw new Error(res.status + ", " + res.statusText); 42 | } 43 | return res.json(); 44 | }) 45 | .then((result) => { 46 | dispatch({ 47 | type: ActionTypes.LOAD_TRANSLATIONS, 48 | data: result 49 | }) 50 | }) 51 | } 52 | } 53 | 54 | export function removeTranslation(id) { 55 | return dispatch => { 56 | return fetch(configUtil.getHost() + '/api/translation/' + id, { 57 | method: 'DELETE' 58 | }) 59 | .then(res => { 60 | if (res.status >= 400) { 61 | throw new Error(res.status + ", " + res.statusText); 62 | } 63 | return res.json(); 64 | }) 65 | .then((data) => { 66 | dispatch({ 67 | type: ActionTypes.REMOVE_TRANSLATION, 68 | id: data.id 69 | }) 70 | }) 71 | } 72 | } 73 | 74 | export function updateTranslation(params) { 75 | return dispatch => { 76 | return fetch(configUtil.getHost() + '/api/translation/' + params._id, { 77 | headers: { 78 | 'Accept': 'application/json; charset=utf-8', 79 | 'Content-Type': 'application/json; charset=utf-8' 80 | }, 81 | method: 'PUT', 82 | body: JSON.stringify(params) 83 | }) 84 | .then(res => { 85 | if (res.status >= 400) { 86 | throw new Error(res.status + ", " + res.statusText); 87 | } 88 | return res.json(); 89 | }) 90 | .then((result) => { 91 | if (result.success) { 92 | dispatch({ 93 | type: ActionTypes.UPDATE_TRANSLATION, 94 | data: result.data 95 | }) 96 | } else { 97 | dispatch({ 98 | type: ActionTypes.ALERT_ERRORS, 99 | errors: result.errors 100 | }) 101 | } 102 | }) 103 | } 104 | } 105 | 106 | /* istanbul ignore next */ 107 | export function importLocale(params) { 108 | let data = new FormData() 109 | data.append('file', params.file) 110 | data.append('locale', params.locale) 111 | data.append('project', params.applyto) 112 | 113 | return dispatch => { 114 | return fetch(configUtil.getHost() + '/api/import', { 115 | method: 'POST', 116 | body: data 117 | }) 118 | .then(res => { 119 | if (res.status >= 400) { 120 | throw new Error(res.status + ", " + res.statusText); 121 | } 122 | return res.json(); 123 | }) 124 | .then((result) => { 125 | if (result.success) { 126 | dispatch({ 127 | type: ActionTypes.IMPORT_LOCALE, 128 | data: result.data 129 | }) 130 | } else { 131 | dispatch({ 132 | type: ActionTypes.ALERT_ERRORS, 133 | errors: result.errors 134 | }) 135 | } 136 | }) 137 | } 138 | } 139 | 140 | export function mergeTranslations(params) { 141 | return dispatch => { 142 | return fetch(configUtil.getHost() + '/api/key', { 143 | headers: { 144 | 'Accept': 'application/json; charset=utf-8', 145 | 'Content-Type': 'application/json; charset=utf-8' 146 | }, 147 | method: 'POST', 148 | body: JSON.stringify(params) 149 | }) 150 | .then(res => { 151 | if (res.status >= 400) { 152 | throw new Error(res.status + ", " + res.statusText); 153 | } 154 | return res.json(); 155 | }) 156 | .then((result) => { 157 | if (result.success) { 158 | dispatch({ 159 | type: ActionTypes.MERGE_TRANSLATIONS, 160 | data: result.data 161 | }) 162 | } 163 | }) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/actions/vis.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes' 2 | import configUtil from '../configUtil' 3 | 4 | export function loadTreeData(projectId) { 5 | return dispatch => { 6 | return fetch(configUtil.getHost() + `/api/vis/tree/${projectId}?t=${+new Date()}`) 7 | .then(res => { 8 | if (res.status >= 400) { 9 | throw new Error(res.status + ", " + res.statusText); 10 | } 11 | return res.json(); 12 | }) 13 | .then((result) => { 14 | dispatch({ 15 | type: ActionTypes.LOAD_TREE_DATA, 16 | data: result 17 | }) 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import { Router } from 'react-router-dom' 5 | import { createBrowserHistory } from 'history' 6 | import configureStore from '../store/configureStore' 7 | import RootContainer from '../containers/RootContainer' 8 | 9 | if (__DEV__) { 10 | require('../../less/app.less'); 11 | } 12 | 13 | ReactDOM.render( 14 | 15 | 16 | 17 | 18 | , 19 | document.getElementById('root') 20 | ); 21 | -------------------------------------------------------------------------------- /src/components/grid/ActionCellRenderer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, memo } from 'react' 2 | import PropTypes from 'prop-types' 3 | import localeUtil from 'keys-translations-manager-core/lib/localeUtil' 4 | 5 | const ActionCellRenderer = memo(({ data, ComponentActions }) => ( 6 | 7 | 11 | 15 | 19 | 20 | )); 21 | 22 | ActionCellRenderer.propTypes = { 23 | data: PropTypes.object, 24 | ComponentActions: PropTypes.object.isRequired, 25 | }; 26 | 27 | export default ActionCellRenderer 28 | -------------------------------------------------------------------------------- /src/components/grid/ConfirmModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Button from 'react-bootstrap/lib/Button' 4 | import Modal from 'react-bootstrap/lib/Modal' 5 | import localeUtil from 'keys-translations-manager-core/lib/localeUtil' 6 | 7 | const ConfirmModal = memo(({ 8 | showconfirmmodal, 9 | closeConfirmModal, 10 | data, 11 | removeTranslation, 12 | }) => ( 13 | 14 | 15 | 16 | {localeUtil.getMsg("ui.common.delete")} 17 | 18 | 19 | 20 | {localeUtil.getMsg("ui.confirm.delete", data.key)} 21 | 22 | 23 | 28 |    29 | 32 | 33 | 34 | )); 35 | 36 | ConfirmModal.propTypes = { 37 | data: PropTypes.object.isRequired, 38 | showconfirmmodal: PropTypes.bool.isRequired, 39 | removeTranslation: PropTypes.func.isRequired, 40 | closeConfirmModal: PropTypes.func.isRequired, 41 | }; 42 | 43 | export default ConfirmModal 44 | -------------------------------------------------------------------------------- /src/components/grid/TablePanel.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Button from 'react-bootstrap/lib/Button' 4 | import InputGroup from 'react-bootstrap/lib/InputGroup' 5 | import FormControl from 'react-bootstrap/lib/FormControl' 6 | import ReactTable from "react-table" 7 | import debounce from 'lodash/debounce'; 8 | import localeUtil from 'keys-translations-manager-core/lib/localeUtil' 9 | import ActionCellRenderer from './ActionCellRenderer' 10 | import Mask from '../layout/Mask' 11 | import { getLocales, getProjectName } from '../../configUtil' 12 | 13 | const locales = getLocales() 14 | 15 | export default class TablePanel extends React.PureComponent { 16 | static propTypes = { 17 | reloaddata: PropTypes.bool, 18 | messages: PropTypes.object, 19 | CountActions: PropTypes.object.isRequired, 20 | TranslationActions: PropTypes.object.isRequired, 21 | ComponentActions: PropTypes.object.isRequired, 22 | translations: PropTypes.array 23 | }; 24 | 25 | constructor(props) { 26 | super(props); 27 | 28 | this.state = { 29 | quickFilterText: null, 30 | windowHeight: 0 31 | }; 32 | 33 | //https://gist.github.com/Restuta/e400a555ba24daa396cc 34 | this.handleResize = this.handleResize.bind(this); 35 | this.debounceResize = debounce(this.handleResize, 150); 36 | this.onQuickFilterText = this.onQuickFilterText.bind(this); 37 | this.debounceFilter = debounce(this.handleFilter, 150); 38 | } 39 | 40 | componentDidMount() { 41 | window.addEventListener('resize', this.debounceResize); 42 | this.loadData(); 43 | } 44 | 45 | componentDidUpdate(prevProps) { 46 | const { reloaddata, translations, CountActions } = this.props; 47 | 48 | if (reloaddata) { 49 | this.loadData(); 50 | } 51 | 52 | if (translations && translations !== prevProps.translations) { 53 | CountActions.loadCounts(); 54 | } 55 | } 56 | 57 | componentWillUnmount() { 58 | window.removeEventListener('resize', this.debounceResize); 59 | } 60 | 61 | loadData() { 62 | this.props.TranslationActions.loadTranslations(); 63 | } 64 | 65 | handleResize() { 66 | this.setState({ 67 | windowHeight: window.innerHeight 68 | }); 69 | } 70 | 71 | handleFilter(quickFilterText) { 72 | this.setState({ 73 | quickFilterText 74 | }); 75 | } 76 | 77 | onQuickFilterText(event) { 78 | this.debounceFilter(event.target.value); 79 | } 80 | 81 | downloadCsv() { 82 | let url = '/api/download/csv' 83 | 84 | /* istanbul ignore next */ 85 | location.href = url; 86 | } 87 | 88 | getColumnDefs() { 89 | return [ 90 | { 91 | Header: localeUtil.getMsg("ui.common.action"), 92 | accessor: '_id', 93 | headerClassName: 'app-grid-header', 94 | width: 85, 95 | Cell: c => 96 | }, { 97 | Header: localeUtil.getMsg("ui.common.applyto"), 98 | accessor: 'project', 99 | headerClassName: 'app-grid-header', 100 | Cell: c => c.value.map(e => getProjectName(e)).join(', '), 101 | }, { 102 | Header: 'Key', 103 | accessor: 'key', 104 | headerClassName: 'app-grid-header', 105 | }, ...locales.map(locale => ({ 106 | Header: `${localeUtil.getMsg("ui.common.locale")} / ${locale}`, 107 | accessor: locale, 108 | headerClassName: 'app-grid-header', 109 | })), { 110 | Header: localeUtil.getMsg("ui.common.desc"), 111 | accessor: 'description', 112 | headerClassName: 'app-grid-header', 113 | } 114 | ]; 115 | } 116 | 117 | render() { 118 | const minHeight = 200, 119 | top = 370, 120 | translations = this.props.translations || [], 121 | data = this.state.quickFilterText 122 | ? translations.filter(e => new RegExp(this.state.quickFilterText, 'i').test(JSON.stringify(e))) 123 | : translations, 124 | windowHeight = this.state.windowHeight || 125 | (typeof window === "undefined" ? minHeight + top : window.innerHeight); 126 | 127 | return ( 128 | 129 | 130 | 131 | 132 | 133 | 136 | 137 | 140 | 141 | 142 | 158 | 159 | 160 | ); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/components/history/DiffPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { getLocales, getProjectName } from '../../configUtil' 4 | import localeUtil from 'keys-translations-manager-core/lib/localeUtil' 5 | 6 | const locales = getLocales() 7 | 8 | function addLeadingZeros(value) { 9 | return ('0' + value).slice(-2) 10 | } 11 | 12 | function formatDateTime(timestamp) { 13 | const d = new Date(timestamp) 14 | const MM = addLeadingZeros(d.getMonth() + 1) 15 | const dd = addLeadingZeros(d.getDate()) 16 | const HH = addLeadingZeros(d.getHours()) 17 | const mm = addLeadingZeros(d.getMinutes()) 18 | const ss = addLeadingZeros(d.getSeconds()) 19 | const formattedDate = `${d.getFullYear()}-${MM}-${dd}` 20 | const formattedTime = `${HH}:${mm}:${ss}` 21 | return `${formattedDate} ${formattedTime} ` 22 | } 23 | 24 | function getActionName(action) { 25 | if (['ADD', 'EDIT', 'DELETE', 'IMPORT', 'MERGE'].indexOf(action) >= 0) { 26 | const a = action.toLowerCase() 27 | return localeUtil.getMsg(`ui.history.${a}`); 28 | } 29 | return action; 30 | } 31 | 32 | function getProjectNames(projects) { 33 | return projects.map(e => getProjectName(e)).join(', ') 34 | } 35 | 36 | function getRow(field, name, comparison) { 37 | const row = [] 38 | if (comparison.original) { 39 | return ( 40 | 41 | {name} 42 | 43 | {comparison.original} 44 | 45 | ) 46 | } 47 | if (comparison.deletion) { 48 | row.push( 49 | 50 | {name} 51 | - 52 | {comparison.deletion} 53 | 54 | ) 55 | } 56 | if (comparison.addition) { 57 | row.push( 58 | 59 | {name} 60 | + 61 | {comparison.addition} 62 | 63 | ) 64 | } 65 | return row 66 | } 67 | 68 | function getProjectRow(name, comparison) { 69 | const c = {} 70 | if (comparison.original) { 71 | c.original = getProjectNames(comparison.original) 72 | } 73 | if (comparison.deletion) { 74 | c.deletion = getProjectNames(comparison.deletion) 75 | } 76 | if (comparison.addition) { 77 | c.addition = getProjectNames(comparison.addition) 78 | } 79 | return getRow('project', name, c) 80 | } 81 | 82 | export default class DiffPanel extends React.PureComponent { 83 | static propTypes = { 84 | log: PropTypes.object.isRequired, 85 | } 86 | 87 | constructor() { 88 | super(); 89 | this.state = { 90 | display: true, 91 | }; 92 | this.displayHandler = this.displayHandler.bind(this); 93 | } 94 | 95 | displayHandler() { 96 | this.setState({ 97 | display: !this.state.display 98 | }); 99 | } 100 | 101 | render() { 102 | const { time, action, user, diff } = this.props.log 103 | const { display } = this.state 104 | 105 | return ( 106 |
107 |
108 | 112 |
113 | {getActionName(action)} 114 | {` ${localeUtil.getMsg('ui.history.at')} `} 115 | {formatDateTime(time)} 116 | 117 | {user && ( 118 | 119 | {` (${localeUtil.getMsg('ui.history.modifier')}: ${user})`} 120 | 121 | )} 122 |
123 |
124 | 125 | 126 | {diff.project && getProjectRow(localeUtil.getMsg('ui.common.applyto'), diff.project)} 127 | {diff.key && getRow('key', 'Key', diff.key)} 128 | {locales.map(el => 129 | diff[el] && getRow(el, el, diff[el]) 130 | )} 131 | {diff.description && getRow('description', localeUtil.getMsg('ui.common.desc'), diff.description)} 132 | 133 |
134 |
135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/components/history/HistoryModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Button from 'react-bootstrap/lib/Button' 4 | import Modal from 'react-bootstrap/lib/Modal' 5 | import localeUtil from 'keys-translations-manager-core/lib/localeUtil' 6 | import historyUtil from 'keys-translations-manager-core/lib/historyUtil' 7 | 8 | import * as Status from '../../constants/Status' 9 | import Spinner from '../layout/Spinner' 10 | import DiffPanel from './DiffPanel' 11 | 12 | export default class HistoryModal extends React.PureComponent { 13 | static propTypes = { 14 | showhistorymodal: PropTypes.bool.isRequired, 15 | closeHistoryModal: PropTypes.func.isRequired, 16 | loadHistory: PropTypes.func.isRequired, 17 | translation: PropTypes.object.isRequired, 18 | historylog: PropTypes.array.isRequired, 19 | historystatus: PropTypes.string.isRequired, 20 | }; 21 | 22 | constructor() { 23 | super(); 24 | this.close = this.close.bind(this); 25 | } 26 | 27 | componentDidUpdate(prevProps) { 28 | const { 29 | showhistorymodal, 30 | translation, 31 | loadHistory, 32 | } = this.props; 33 | 34 | if ( 35 | showhistorymodal && translation && translation._id && 36 | showhistorymodal !== prevProps.showhistorymodal 37 | ) { 38 | loadHistory(translation._id); 39 | } 40 | } 41 | 42 | close() { 43 | this.props.closeHistoryModal() 44 | } 45 | 46 | render() { 47 | const { 48 | showhistorymodal, 49 | translation, 50 | historylog, 51 | historystatus, 52 | } = this.props; 53 | const logs = []; 54 | let len = historylog ? historylog.length : 0, 55 | i = len - 1, 56 | diff, 57 | prev; 58 | 59 | for (; i >= 0; i--) { 60 | if (i === 0) { 61 | prev = ['ADD', 'IMPORT'].includes(historylog[0].action) ? null : translation; 62 | } else { 63 | prev = historylog[i - 1].translation; 64 | } 65 | diff = historyUtil.differentiate(prev, historylog[i].translation); 66 | if (diff) { 67 | historylog[i].diff = diff; 68 | logs.push(historylog[i]); 69 | } 70 | } 71 | 72 | return ( 73 | 74 | 75 | 76 | {`${localeUtil.getMsg('ui.common.history')} (ID: ${translation._id})`} 77 | 78 | 79 | 80 | { 81 | historystatus === Status.STATUS_FETCHING 82 | ? 83 | : (logs.length 84 | // Can't use el._id as a key (cause 'MERGE' have no log._id) 85 | ? logs.map((log, i) => ) 86 | : localeUtil.getMsg('ui.history.none') 87 | ) 88 | } 89 | 90 | 91 | 94 | 95 | 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/components/input/AlertPanel.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Alert from 'react-bootstrap/lib/Alert' 4 | import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger' 5 | import Tooltip from 'react-bootstrap/lib/Tooltip' 6 | import localeUtil from 'keys-translations-manager-core/lib/localeUtil' 7 | import configUtil from '../../configUtil' 8 | 9 | const getProjectName = configUtil.getProjectName 10 | const num = 3 11 | 12 | const AlertPanel = memo(({ clearErrors, errors, action }) => { 13 | const len = errors.length, 14 | errMsg = []; 15 | let err, 16 | i, 17 | counter = 0; 18 | 19 | for (i=0; i `"${getProjectName(e)}"`).join(", ") 29 | )); 30 | break; 31 | case 'contains': 32 | errMsg.push(localeUtil.getMsg("ui.err.contains", err.params.key, err.key, 33 | err.match.map(e => `"${getProjectName(e)}"`).join(", ") 34 | )); 35 | break; 36 | case 'belongsTo': 37 | errMsg.push(localeUtil.getMsg("ui.err.belongsTo", err.params.key, err.key, 38 | err.match.map(e => `"${getProjectName(e)}"`).join(", ") 39 | )); 40 | break; 41 | case 'emptyfield': 42 | errMsg.push(localeUtil.getMsg("ui.err.emptyfield", 43 | err.match.map(e => `"${e}"`).join(", ") 44 | )); 45 | break; 46 | case 'accept': 47 | errMsg.push( 48 | localeUtil.getMsg("ui.file.accept", err.match.map(e => `*.${e}`) 49 | .join(` ${localeUtil.getMsg("ui.common.or")} `) 50 | )); 51 | break; 52 | case 'iequals': 53 | case 'iconflicts': 54 | errMsg.push( 55 | 56 | {localeUtil.getMsg("ui.err." + err.type)} 57 |   58 | {localeUtil.getMsg("ui.tip." + err.type)}}> 59 | 60 | 61 |
      62 | {err.key.length >= (num + 2) 63 | ? `${err.key.slice(0, num).join(", ")} ${localeUtil.getMsg("ui.common.others", err.key.length - num)}` 64 | : err.key.join(", ") 65 | } 66 |
67 | ); 68 | break; 69 | default: 70 | errMsg.push(err.type); 71 | break; 72 | } 73 | } 74 | 75 | return (errMsg.length > 0) ? ( 76 | {errMsg.map(e =>

{e}

)} 77 |
) : (action === "c" ?
:
); 78 | }); 79 | 80 | AlertPanel.propTypes = { 81 | clearErrors: PropTypes.func.isRequired, 82 | errors: PropTypes.array, 83 | action: PropTypes.string.isRequired 84 | }; 85 | 86 | export default AlertPanel 87 | -------------------------------------------------------------------------------- /src/components/input/EditModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Button from 'react-bootstrap/lib/Button' 4 | import Modal from 'react-bootstrap/lib/Modal' 5 | import FormPanel from './FormPanel' 6 | import AlertPanel from './AlertPanel' 7 | import localeUtil from 'keys-translations-manager-core/lib/localeUtil' 8 | import configUtil from '../../configUtil' 9 | 10 | const locales = configUtil.getLocales() 11 | const lenProjects = configUtil.getProjects().length 12 | 13 | export default class EditModal extends React.PureComponent { 14 | static propTypes = { 15 | showeditmodal: PropTypes.bool.isRequired, 16 | closeEditModal: PropTypes.func.isRequired, 17 | data: PropTypes.object.isRequired, 18 | errors: PropTypes.array.isRequired, 19 | updateTranslation: PropTypes.func.isRequired, 20 | alertErrors: PropTypes.func.isRequired, 21 | clearErrors: PropTypes.func.isRequired 22 | }; 23 | 24 | constructor() { 25 | super(); 26 | this.updateTranslation = this.updateTranslation.bind(this); 27 | this.close = this.close.bind(this); 28 | } 29 | 30 | /* istanbul ignore next */ 31 | updateTranslation() { 32 | const el = this.refFormPanel.getFormElements(), 33 | projects = el["project[]"], 34 | lenLocales = locales.length, 35 | project = [], 36 | emptyFields = [], 37 | data = { 38 | ...this.props.data, 39 | description: el.description.value.trim() 40 | }; 41 | let k, i, v, locale; 42 | 43 | k = el.key.value.trim() 44 | if (k) { 45 | data.key = k 46 | } else { 47 | emptyFields.push("Key") 48 | } 49 | 50 | for (i = 0; i < lenLocales; i++) { 51 | locale = locales[i] 52 | v = el[locale].value.trim() 53 | if (v) { 54 | data[locale] = v 55 | } else { 56 | emptyFields.push(localeUtil.getMsg("ui.common.locale") + " / " + locale) 57 | } 58 | } 59 | 60 | if (lenProjects === 1) { // projects would be an object, not an array 61 | if (projects.checked) { 62 | project.push(projects.value); 63 | } 64 | } else { 65 | for (i = 0; i < lenProjects; i++) { 66 | if (projects[i] && projects[i].checked) { 67 | project.push(projects[i].value); 68 | } 69 | } 70 | } 71 | if (project.length > 0) { 72 | data.project = project 73 | } else { 74 | emptyFields.push(localeUtil.getMsg("ui.common.applyto")) 75 | } 76 | 77 | if (emptyFields.length > 0) { 78 | this.props.alertErrors([{ 79 | type: 'emptyfield', 80 | action: "u", 81 | params: data, 82 | match: emptyFields 83 | }]); 84 | } else { 85 | this.props.updateTranslation(data); 86 | } 87 | } 88 | 89 | close() { 90 | this.props.closeEditModal() 91 | } 92 | 93 | render() { 94 | const { showeditmodal, data, errors, clearErrors } = this.props; 95 | 96 | return ( 97 | 98 | 99 | 100 | {localeUtil.getMsg("ui.common.edit")} 101 | 102 | 103 | 104 | 105 | { this.refFormPanel = cmp; }} 107 | action="u" data={data} 108 | /> 109 | 110 | 111 | 114 | 117 | 118 | 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/components/input/FormPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import PropTypes from 'prop-types' 4 | import Checkbox from 'react-bootstrap/lib/Checkbox' 5 | import localeUtil from 'keys-translations-manager-core/lib/localeUtil' 6 | import TextField from './TextField' 7 | import configUtil from '../../configUtil' 8 | 9 | const locales = configUtil.getLocales(); 10 | const projects = configUtil.getProjects(); 11 | 12 | export default class FormPanel extends React.PureComponent { 13 | static propTypes = { 14 | data: PropTypes.object, 15 | }; 16 | 17 | constructor(props) { 18 | super(props); 19 | 20 | const { data } = props; 21 | let lenLocales = locales.length, 22 | lenProjects = projects.length, 23 | locale, 24 | project, 25 | o; 26 | 27 | if (data) { //update 28 | o = { 29 | action: "u", 30 | errors: [], 31 | key: data.key, 32 | description: data.description 33 | } 34 | while (lenLocales--) { 35 | locale = locales[lenLocales] 36 | o[locale] = data[locale] 37 | } 38 | while (lenProjects--) { 39 | project = projects[lenProjects] 40 | o[project.id] = (data.project.indexOf(project.id) >= 0) 41 | } 42 | } else { //create 43 | o = { 44 | action: "c" 45 | } 46 | while (lenLocales--) { 47 | locale = locales[lenLocales] 48 | o[locale] = "" 49 | } 50 | while (lenProjects--) { 51 | project = projects[lenProjects] 52 | o[project.id] = false 53 | } 54 | } 55 | 56 | this.state = o; 57 | } 58 | 59 | getFormElements() { 60 | const form = ReactDOM.findDOMNode(this.refForm) 61 | return form.elements 62 | } 63 | 64 | onCheckboxChange(id) { 65 | let o = {}; 66 | o[id] = !this.state[id]; 67 | this.setState(o); 68 | } 69 | 70 | render() { 71 | const { data } = this.props, 72 | lenLocales = locales.length, 73 | lenProjects = projects.length, 74 | getLabel = (key, text) =>
* {text}:
75 | let i, p, locale, 76 | projectGroup = [], 77 | localeGroup = []; 78 | 79 | for (i = 0; i < lenLocales; i++) { 80 | locale = locales[i] 81 | localeGroup.push( 82 | 86 | ) 87 | } 88 | for (i = 0; i < lenProjects; i++) { 89 | p = projects[i]; 90 | projectGroup.push( 91 | 93 | {p.name} 94 | 95 | ) 96 | } 97 | 98 | return( 99 |
{ this.refForm = cmp; }}> 100 | {(this.state.action === "u") 101 | ? 102 | : 103 | } 104 | 105 | {localeGroup} 106 | 107 | 110 | 111 |
112 | {getLabel("applyTo", localeUtil.getMsg("ui.common.applyto"))} 113 |
114 | 115 |
116 | {projectGroup} 117 |
118 | 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/components/input/InputPanel.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Button from 'react-bootstrap/lib/Button' 4 | import FormPanel from './FormPanel' 5 | import localeUtil from 'keys-translations-manager-core/lib/localeUtil' 6 | import configUtil from '../../configUtil' 7 | 8 | const lenProjects = configUtil.getProjects().length 9 | 10 | export default class InputPanel extends React.PureComponent { 11 | static propTypes = { 12 | messages: PropTypes.object.isRequired, 13 | addTranslation: PropTypes.func.isRequired, 14 | alertErrors: PropTypes.func.isRequired, 15 | }; 16 | 17 | addTranslation() { 18 | const el = this.refFormPanel.getFormElements(), 19 | projects = el["project[]"], 20 | locales = configUtil.getLocales(), 21 | lenLocales = locales.length; 22 | 23 | let i, vk, vl, locale, project = [], emptyFields = [], 24 | data = { description: el.description.value.trim() }; 25 | 26 | vk = el.key.value.trim() 27 | if (vk) { 28 | data.key = vk 29 | } else { 30 | emptyFields.push("Key") 31 | } 32 | 33 | for (i = 0; i < lenLocales; i++) { 34 | locale = locales[i] 35 | vl = el[locale].value.trim() 36 | if (vl) { 37 | data[locale] = vl 38 | } else { 39 | emptyFields.push(localeUtil.getMsg("ui.common.locale") + " / " + locale) 40 | } 41 | } 42 | 43 | if (lenProjects === 1) { // projects would be an object, not an array 44 | if (projects.checked) { 45 | project.push(projects.value); 46 | } 47 | } else { 48 | for (i = 0; i < lenProjects; i++) { 49 | if (projects[i] && projects[i].checked) { 50 | project.push(projects[i].value); 51 | } 52 | } 53 | } 54 | if (project.length > 0) { 55 | data.project = project 56 | } else { 57 | emptyFields.push(localeUtil.getMsg("ui.common.applyto")) 58 | } 59 | 60 | if (emptyFields.length > 0) { 61 | this.props.alertErrors([{ 62 | type: 'emptyfield', 63 | action: "c", 64 | params: data, 65 | match: emptyFields 66 | }]); 67 | } else { 68 | this.props.addTranslation(data); 69 | } 70 | } 71 | 72 | render() { 73 | return( 74 | 75 | { this.refFormPanel = cmp; }} 79 | /> 80 |
81 | 86 |
87 |
88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/components/input/TextField.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import PropTypes from 'prop-types' 3 | import FormGroup from 'react-bootstrap/lib/FormGroup' 4 | import ControlLabel from 'react-bootstrap/lib/ControlLabel' 5 | import FormControl from 'react-bootstrap/lib/FormControl' 6 | 7 | const TextField = memo(({ required, label, value, ...restProps }) => { 8 | // restProps: name, defaultValue, onChange, readOnly, className, placeholder 9 | const style = value ? { backgroundColor: "#e7e7e7" } : {} 10 | return( 11 | 12 | {label && 13 | ( 14 | {required 15 | ? * 16 | : null} 17 | {label}: 18 | ) 19 | } 20 | 21 | 22 | ) 23 | }); 24 | 25 | TextField.propTypes = { 26 | required: PropTypes.bool, 27 | readOnly: PropTypes.bool, 28 | label: PropTypes.string, 29 | name: PropTypes.string.isRequired, 30 | value: PropTypes.string, 31 | defaultValue: PropTypes.string, 32 | placeholder: PropTypes.string, 33 | className: PropTypes.string, 34 | componentClass: PropTypes.string, 35 | onChange: PropTypes.func 36 | }; 37 | 38 | export default TextField 39 | -------------------------------------------------------------------------------- /src/components/layout/DropdownMenu.jsx: -------------------------------------------------------------------------------- 1 | /*eslint i18n/no-chinese-character: 0*/ 2 | import React, { memo } from 'react' 3 | import PropTypes from 'prop-types' 4 | import localeUtil from 'keys-translations-manager-core/lib/localeUtil' 5 | 6 | const DropdownMenu = memo(({ lang, loadMessages, showImportModal, findMergeable }) => { 7 | const loadMsg = ln => { 8 | if (ln !== lang) { 9 | loadMessages(ln); 10 | } 11 | } 12 | return ( 13 | 72 | ); 73 | }); 74 | 75 | DropdownMenu.propTypes = { 76 | lang: PropTypes.string.isRequired, 77 | loadMessages: PropTypes.func.isRequired, 78 | showImportModal: PropTypes.func.isRequired, 79 | findMergeable: PropTypes.func.isRequired 80 | }; 81 | 82 | export default DropdownMenu 83 | -------------------------------------------------------------------------------- /src/components/layout/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | export default memo(function Header() { 4 | return ( 5 |
6 | 17 | 18 | 19 | {' '} 20 | Keys-Translations Manager 21 | 22 |
23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/layout/MainPanel.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Row from 'react-bootstrap/lib/Row' 4 | import Col from 'react-bootstrap/lib/Col' 5 | 6 | const MainPanel = memo(props => ( 7 | 8 | 9 | {/*
10 |
11 |
*/} 12 | {props.children} 13 | 14 |
15 | )); 16 | 17 | MainPanel.propTypes = { 18 | children: PropTypes.node 19 | }; 20 | 21 | export default MainPanel 22 | -------------------------------------------------------------------------------- /src/components/layout/Mask.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Modal from 'react-bootstrap/lib/Modal' 4 | import Spinner from './Spinner' 5 | 6 | const Mask = memo(({ show }) => ( 7 | 8 | 9 | 10 | 11 | 12 | )); 13 | 14 | Mask.propTypes = { 15 | show: PropTypes.bool.isRequired 16 | }; 17 | 18 | export default Mask 19 | -------------------------------------------------------------------------------- /src/components/layout/MessagePopup.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const MessagePopup = memo(({ 5 | children, msg, showmessagepopup, closeMessagePopup 6 | }) => { 7 | const style = { 8 | display: showmessagepopup ? "block" : "none" 9 | } 10 | 11 | return ( 12 |
13 |
14 | 15 | {msg} 16 | {' '} 17 | {children} 18 |
19 |
20 | ); 21 | }); 22 | 23 | MessagePopup.propTypes = { 24 | children: PropTypes.node, 25 | msg: PropTypes.string.isRequired, 26 | showmessagepopup: PropTypes.bool.isRequired, 27 | closeMessagePopup: PropTypes.func.isRequired 28 | }; 29 | 30 | export default MessagePopup 31 | -------------------------------------------------------------------------------- /src/components/layout/SideBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const SideBar = memo(props => ( 5 |
6 |
7 | 18 |
19 |
20 | )); 21 | 22 | SideBar.propTypes = { 23 | children: PropTypes.node 24 | }; 25 | 26 | export default SideBar 27 | -------------------------------------------------------------------------------- /src/components/layout/Spinner.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | 3 | const Spinner = memo(() => ( 4 |
5 | 6 |
7 | )); 8 | 9 | export default Spinner 10 | -------------------------------------------------------------------------------- /src/components/merge/MergeModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Button from 'react-bootstrap/lib/Button' 4 | import Modal from 'react-bootstrap/lib/Modal' 5 | import localeUtil from 'keys-translations-manager-core/lib/localeUtil' 6 | 7 | const num = 10 8 | 9 | const MergeModal = memo(({ 10 | keys, mergeable, showmergemodal, closeMergeModal, mergeTranslations 11 | }) => { 12 | const k = Object.keys(keys).map(key => key) 13 | const submit = () => { 14 | mergeTranslations(mergeable) 15 | } 16 | 17 | return ( 18 | 19 | 20 | 21 | {localeUtil.getMsg("ui.common.merge")} 22 | 23 | 24 | 25 | { 26 | k.length > 0 27 | ? (
28 | {localeUtil.getMsg("ui.merge.match")} 29 | {k.length >= (num + 2) 30 | ? `${k.slice(0, num).join(", ")} ${localeUtil.getMsg("ui.common.others", k.length - num)}` 31 | : k.join(", ")} 32 |

33 | {localeUtil.getMsg("ui.confirm.continue")} 34 |
) 35 | : localeUtil.getMsg("ui.merge.nomatch") 36 | } 37 |
38 | {k.length > 0 ? 39 | 40 | 43 | 46 | : 47 | 48 | 51 | 52 | } 53 |
54 | ); 55 | }); 56 | 57 | MergeModal.propTypes = { 58 | keys: PropTypes.object.isRequired, 59 | mergeable: PropTypes.array.isRequired, 60 | showmergemodal: PropTypes.bool.isRequired, 61 | closeMergeModal: PropTypes.func.isRequired, 62 | mergeTranslations: PropTypes.func.isRequired 63 | }; 64 | 65 | export default MergeModal; 66 | -------------------------------------------------------------------------------- /src/components/output/CountCol.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'react-router-dom' 4 | import Col from 'react-bootstrap/lib/Col' 5 | import localeUtil from 'keys-translations-manager-core/lib/localeUtil' 6 | 7 | const CountCol = memo(({ projectId, header, onClick, count, desc }) => ( 8 | 9 |
10 |
11 |
12 | {header} 13 |
14 |
15 | 21 |
22 |
23 |
24 |
25 | 26 | {count ? {count} : count} 27 | 28 |
29 |
30 | {desc} 31 |
32 |
33 |
34 | 35 | )); 36 | 37 | CountCol.propTypes = { 38 | projectId: PropTypes.string.isRequired, 39 | header: PropTypes.string.isRequired, 40 | onClick: PropTypes.func.isRequired, 41 | count: PropTypes.number.isRequired, 42 | desc: PropTypes.string.isRequired 43 | }; 44 | 45 | export default CountCol; 46 | -------------------------------------------------------------------------------- /src/components/output/FileTypeCol.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Col from 'react-bootstrap/lib/Col' 4 | 5 | const FileTypeCol = memo(({ value, fileType, label, onChange }) => ( 6 | 7 | 14 | {' '} 15 | {label} 16 | 17 | )); 18 | 19 | FileTypeCol.propTypes = { 20 | value: PropTypes.string.isRequired, 21 | fileType: PropTypes.string.isRequired, 22 | label: PropTypes.string.isRequired, 23 | onChange: PropTypes.func.isRequired 24 | }; 25 | 26 | export default FileTypeCol; 27 | -------------------------------------------------------------------------------- /src/components/output/OutputPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Well from 'react-bootstrap/lib/Well' 4 | import Row from 'react-bootstrap/lib/Row' 5 | import localeUtil from 'keys-translations-manager-core/lib/localeUtil' 6 | import configUtil from '../../configUtil' 7 | import CountCol from './CountCol' 8 | import FileTypeCol from './FileTypeCol' 9 | 10 | export default class OutputPanel extends React.PureComponent { 11 | static propTypes = { 12 | projectCounts: PropTypes.object.isRequired 13 | }; 14 | 15 | constructor() { 16 | super(); 17 | this.state = { 18 | fileType: "nj" 19 | }; 20 | } 21 | 22 | setFileType(fileType) { 23 | this.setState({ 24 | fileType: fileType 25 | }); 26 | } 27 | 28 | download(project) { 29 | let url = '/api/download/' 30 | 31 | /* istanbul ignore next */ 32 | if (this.state.fileType === 'njf' || this.state.fileType === 'fjf') { 33 | url += 'f/'; 34 | } else { 35 | url += 'n/'; 36 | } 37 | 38 | /* istanbul ignore next */ 39 | if (this.state.fileType === 'p') { 40 | url += 'properties/'; 41 | } else if (this.state.fileType === 'fj' || this.state.fileType === 'fjf') { 42 | url += 'flat/'; 43 | } else { 44 | url += 'json/'; 45 | } 46 | 47 | /* istanbul ignore next */ 48 | location.href = url + project.id; 49 | } 50 | 51 | render() { 52 | const me = this 53 | const { projectCounts } = this.props 54 | const fileTypeList = [{ 55 | value: "nj", label: `nested JSON (${localeUtil.getMsg("ui.json.mini")})` 56 | }, { 57 | value: "njf", label: `nested JSON (${localeUtil.getMsg("ui.json.format")})` 58 | }, { 59 | value: "fj", label: `flat JSON (${localeUtil.getMsg("ui.json.mini")})` 60 | }, { 61 | value: "fjf", label: `flat JSON (${localeUtil.getMsg("ui.json.format")})` 62 | }, { 63 | value: "p", label: "Properties" 64 | }] 65 | 66 | return( 67 | 68 | 69 | {fileTypeList.map(e => ( 70 | 73 | ))} 74 | 75 | 76 | {configUtil.getProjects().map(e => ( 77 | 83 | ))} 84 | 85 | 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/components/vis/Tooltip.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | import PropTypes from 'prop-types' 3 | import timingUtil from 'keys-translations-manager-core/lib/timingUtil' 4 | 5 | const Tooltip = memo(({ children, display, top, left, ComponentActions }) => { 6 | const style = { display, top, left } 7 | const onMouseOver = () => { 8 | clearInterval(timingUtil.getTimeoutId()); 9 | // ComponentActions.hideTooltip(0, 0); 10 | } 11 | const onMouseOut = () => { 12 | const timeoutId = setTimeout(() => { 13 | ComponentActions.hideTooltip(); 14 | }, 200); 15 | timingUtil.setTimeoutId(timeoutId); 16 | } 17 | 18 | return ( 19 | 25 | {children} 26 | 27 | ); 28 | }); 29 | 30 | Tooltip.propTypes = { 31 | children: PropTypes.node, 32 | display: PropTypes.string.isRequired, 33 | top: PropTypes.number.isRequired, 34 | left: PropTypes.number.isRequired, 35 | ComponentActions: PropTypes.object.isRequired 36 | }; 37 | 38 | export default Tooltip 39 | -------------------------------------------------------------------------------- /src/configUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const config = require('../ktm.config'), 3 | { locales, projects } = config, 4 | projectIdList = [], 5 | projectNames = {}; 6 | 7 | projects.map(e => { 8 | projectIdList.push(e.id); 9 | projectNames[e.id] = e.name; 10 | }); 11 | 12 | module.exports = { 13 | getHost() { 14 | if (process.env.NODE_ENV === 'test') { 15 | return `http://localhost:${process.env.PORT || 3000}`; 16 | } 17 | return ''; 18 | }, 19 | getLocales() { 20 | return locales; 21 | }, 22 | getProjects() { 23 | return projects; 24 | }, 25 | getProjectIdList() { 26 | return projectIdList; 27 | }, 28 | getProjectName(projectId) { 29 | return projectNames[projectId]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const LOAD_MESSAGES = 'LOAD_MESSAGES'; 2 | 3 | export const LOAD_COUNTS = 'LOAD_COUNTS'; 4 | 5 | export const ALERT_ERRORS = 'ALERT_ERRORS'; 6 | export const CLEAR_ERRORS = 'CLEAR_ERRORS'; 7 | 8 | export const ADD_TRANSLATION = 'ADD_TRANSLATION'; 9 | export const LOAD_TRANSLATIONS = 'LOAD_TRANSLATIONS'; 10 | export const REMOVE_TRANSLATION = 'REMOVE_TRANSLATION'; 11 | export const UPDATE_TRANSLATION = 'UPDATE_TRANSLATION'; 12 | export const IMPORT_LOCALE = 'IMPORT_LOCALE'; 13 | export const MERGE_TRANSLATIONS = 'MERGE_TRANSLATIONS'; 14 | 15 | export const FIND_MERGEABLE = 'FIND_MERGEABLE'; 16 | 17 | export const LOAD_HISTORY = 'LOAD_HISTORY'; 18 | 19 | export const SHOW_EDITMODAL = 'SHOW_EDITMODAL'; 20 | export const CLOSE_EDITMODAL = 'CLOSE_EDITMODAL'; 21 | export const SHOW_CONFIRMMODAL = 'SHOW_CONFIRMMODAL'; 22 | export const CLOSE_CONFIRMMODAL = 'CLOSE_CONFIRMMODAL'; 23 | export const SHOW_HISTORYMODAL = 'SHOW_HISTORYMODAL'; 24 | export const CLOSE_HISTORYMODAL = 'CLOSE_HISTORYMODAL'; 25 | export const CLOSE_MERGEMODAL = 'CLOSE_MERGEMODAL'; 26 | export const SHOW_IMPORTMODAL = 'SHOW_IMPORTMODAL'; 27 | export const CLOSE_IMPORTMODAL = 'CLOSE_IMPORTMODAL'; 28 | export const RELOAD_DATA = 'RELOAD_DATA'; 29 | 30 | export const SHOW_MESSAGEPOPUP = 'SHOW_MESSAGEPOPUP'; 31 | export const CLOSE_MESSAGEPOPUP = 'CLOSE_MESSAGEPOPUP'; 32 | 33 | export const END_DATACHANGE = 'END_DATACHANGE'; 34 | 35 | export const LOAD_TREE_DATA = 'LOAD_TREE_DATA'; 36 | 37 | export const SHOW_TOOLTIP = 'SHOW_TOOLTIP'; 38 | export const HIDE_TOOLTIP = 'HIDE_TOOLTIP'; 39 | -------------------------------------------------------------------------------- /src/constants/InitStates.js: -------------------------------------------------------------------------------- 1 | import * as Status from './Status' 2 | 3 | export const INIT_COMPONENTS = { 4 | reloaddata: false, 5 | showimportmodal: false, 6 | showmergemodal: false, 7 | showeditmodal: false, 8 | showconfirmmodal: false, 9 | showhistorymodal: false, 10 | showmessagepopup: false, 11 | showtooltip: false, 12 | tooltiptop: 0, 13 | tooltipleft: 0, 14 | keys: {}, 15 | mergeable: [], 16 | editrecord: {}, 17 | }; 18 | export const INIT_SOCKET = { 19 | emitdatachange: false 20 | }; 21 | export const INIT_COUNTS = {}; 22 | export const INIT_ERRORS = []; 23 | export const INIT_MESSAGES = { 24 | lang: '', 25 | messages: {} 26 | }; 27 | export const INIT_TRANSLATIONS = null; 28 | export const INIT_HISTORY = { 29 | historylog: [], 30 | historystatus: Status.STATUS_FETCHED, 31 | }; 32 | export const INIT_VIS = { 33 | treedata: null 34 | }; 35 | -------------------------------------------------------------------------------- /src/constants/Languages.js: -------------------------------------------------------------------------------- 1 | export const LANGUAGES = ["en-US", "zh-TW"]; 2 | -------------------------------------------------------------------------------- /src/constants/Status.js: -------------------------------------------------------------------------------- 1 | export const STATUS_FETCHED = 'FETCHED'; 2 | export const STATUS_FETCHING = 'FETCHING'; 3 | export const STATUS_ERROR = 'ERROR'; 4 | -------------------------------------------------------------------------------- /src/containers/RootContainer.js: -------------------------------------------------------------------------------- 1 | import ES6Promise from 'es6-promise' 2 | ES6Promise.polyfill(); 3 | import 'isomorphic-fetch' 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | import { withRouter } from 'react-router' 7 | import * as MessageActions from '../actions/messages' 8 | import * as CountActions from '../actions/counts' 9 | import * as TranslationActions from '../actions/translations' 10 | import * as HistoryActions from '../actions/history' 11 | import * as KeyActions from '../actions/keys' 12 | import * as ErrorActions from '../actions/errors' 13 | import * as SocketActions from '../actions/socket' 14 | import * as ComponentActions from '../actions/components' 15 | import App from '../App' 16 | 17 | function mapStateToProps(state) { 18 | return { 19 | counts: state.counts, 20 | errors: state.errors, 21 | translations: state.translations, 22 | emitdatachange: state.socket.emitdatachange, 23 | showeditmodal: state.components.showeditmodal, 24 | showconfirmmodal: state.components.showconfirmmodal, 25 | showhistorymodal: state.components.showhistorymodal, 26 | showmergemodal: state.components.showmergemodal, 27 | showimportmodal: state.components.showimportmodal, 28 | showmessagepopup: state.components.showmessagepopup, 29 | reloaddata: state.components.reloaddata, 30 | keys: state.components.keys, 31 | mergeable: state.components.mergeable, 32 | editrecord: state.components.editrecord, 33 | ...state.messages, 34 | ...state.history, 35 | } 36 | } 37 | 38 | function mapDispatchToProps(dispatch) { 39 | return { 40 | MessageActions: bindActionCreators(MessageActions, dispatch), 41 | CountActions: bindActionCreators(CountActions, dispatch), 42 | TranslationActions: bindActionCreators(TranslationActions, dispatch), 43 | HistoryActions: bindActionCreators(HistoryActions, dispatch), 44 | KeyActions: bindActionCreators(KeyActions, dispatch), 45 | ErrorActions: bindActionCreators(ErrorActions, dispatch), 46 | SocketActions: bindActionCreators(SocketActions, dispatch), 47 | ComponentActions: bindActionCreators(ComponentActions, dispatch) 48 | } 49 | } 50 | 51 | // Dealing with Update Blocking 52 | // - https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/guides/blocked-updates.md 53 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App)) 54 | -------------------------------------------------------------------------------- /src/containers/VisContainer.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux' 2 | import { connect } from 'react-redux' 3 | import * as VisActions from '../actions/vis' 4 | import Tree from '../components/vis/Tree' 5 | 6 | const mapDispatchToProps = (dispatch) => ({ 7 | VisActions: bindActionCreators(VisActions, dispatch) 8 | }) 9 | 10 | const mapStateToProps = (state) => ({ 11 | showtooltip: state.components.showtooltip, 12 | tooltiptop: state.components.tooltiptop, 13 | tooltipleft: state.components.tooltipleft, 14 | treedata: state.vis.treedata 15 | }) 16 | 17 | export default connect(mapStateToProps, mapDispatchToProps)(Tree) 18 | -------------------------------------------------------------------------------- /src/controllers/CountController.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import Translations from '../models/TranslationModel' 3 | import configUtil from '../configUtil' 4 | const projectIdList = configUtil.getProjectIdList() 5 | const router = express.Router() 6 | 7 | router.route('/projects') 8 | .get(function(req, res) { 9 | Translations.aggregate([{ 10 | "$match": { 11 | "project": { 12 | "$in": projectIdList 13 | } 14 | } 15 | }, { 16 | "$unwind": "$project" 17 | }, { 18 | "$group": { 19 | "_id": '$project', 20 | "count": {"$sum": 1} 21 | } 22 | }], function (err, result) { 23 | if (err) { 24 | res.status(500).send(err); 25 | } 26 | res.json(result); 27 | }); 28 | }); 29 | 30 | export default router 31 | -------------------------------------------------------------------------------- /src/controllers/DownloadController.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import archiver from 'archiver' 3 | import transformationUtil from 'keys-translations-manager-core/lib/transformationUtil' 4 | import Translations from '../models/TranslationModel' 5 | import config from '../../ktm.config' 6 | const locales = config.locales 7 | const lenLocales = locales.length 8 | const router = express.Router() 9 | const document2FileContent = transformationUtil.document2FileContent 10 | 11 | router.route('/:outputType/:fileType/:project/:locale') 12 | .get(function(req, res) { 13 | // outputType (f: format, n: none) 14 | // fileType (json, flat, properties) 15 | const { outputType, fileType, project, locale } = req.params 16 | 17 | let query, 18 | criteria = { 19 | "project": project 20 | }, 21 | select = { 22 | "_id": 0, 23 | "key": 1 24 | }; 25 | 26 | criteria[locale] = {$exists: true}; 27 | select[locale] = 1; 28 | query = Translations.find(criteria).select(select); 29 | if (outputType === "f") { 30 | query.sort({'key':-1}); 31 | } 32 | query.exec(function(err, translations) { 33 | let str, 34 | formatted = outputType === "f"; 35 | 36 | if (err) { 37 | res.status(500).send(err); 38 | } 39 | 40 | str = document2FileContent(translations, locale, fileType, formatted); 41 | 42 | if (fileType === "json" || fileType === "flat") { 43 | res.set({ 44 | ...(req.baseUrl === '/api/rest' 45 | ? {} 46 | : { "Content-Disposition": "attachment; filename=\"translation.json\"" } 47 | ), 48 | "Content-Type": "application/json; charset=utf-8" 49 | }); 50 | } else if (fileType === "properties") { 51 | res.set({ 52 | ...(req.baseUrl === '/api/rest' 53 | ? {} 54 | : { "Content-Disposition": "attachment; filename=\"translation.properties\"" } 55 | ), 56 | "Content-Type": "text/x-java-properties; charset=utf-8" 57 | }); 58 | } 59 | res.send(str); 60 | 61 | }); 62 | }); 63 | 64 | router.route('/:outputType/:fileType/:project') 65 | .get(function(req, res) { 66 | // outputType (f: format, n: none) 67 | // fileType (json, flat, properties) 68 | const { outputType, fileType, project } = req.params, 69 | archive = archiver.create('zip', {}), 70 | zipHandler = function(stream, locale, fileExt) { 71 | archive.append(stream, { name: locale + '/translation.' + fileExt }); 72 | if (++count === lenLocales) { 73 | archive.finalize(); 74 | } 75 | }; 76 | 77 | let query, 78 | criteria = {}, 79 | select = { 80 | "_id": 0, 81 | "key": 1 82 | }, 83 | count = 0, 84 | locale; 85 | 86 | res.set({ 87 | 'Content-Type': 'application/zip', 88 | 'Content-disposition': 'attachment; filename=translations.zip' 89 | }); 90 | archive.pipe(res); 91 | 92 | for (let i = 0; i < lenLocales; i++) { 93 | locale = locales[i]; 94 | 95 | criteria.project = project; 96 | select[locale] = 1; 97 | query = Translations.find(criteria).select(select); 98 | if (outputType === "f") { 99 | query.sort({'key':-1}); 100 | } 101 | query.exec(function(err, translations) { 102 | let str, 103 | locale = this, 104 | formatted = outputType === "f", 105 | finalFileType = fileType === "flat" ? "json" : fileType; 106 | 107 | if (err) { 108 | res.status(500).send(err); 109 | } 110 | 111 | str = document2FileContent(translations, locale, fileType, formatted); 112 | zipHandler(str, locale, finalFileType); 113 | }.bind(locale)); 114 | } 115 | }); 116 | 117 | router.route('/csv') 118 | .get(function(req, res) { 119 | Translations.find({}, null, {sort: {'_id': 1}}, function(err, translations) { 120 | const delimiter = "\t" 121 | let len = translations.length, 122 | translation, 123 | i, 124 | str; 125 | 126 | if (err) { 127 | res.status(500).send(err); 128 | } 129 | 130 | res.set({ 131 | "Content-Disposition": "attachment; filename=\"translations.csv\"", 132 | "Content-Type": "text/csv; charset=utf-8" 133 | }); 134 | 135 | // header 136 | str = ""; 137 | for (i = 0; i < lenLocales; i++) { 138 | str += delimiter + locales[i]; 139 | } 140 | res.write(`Project${delimiter}Key${delimiter}Description${str}\r\n`); 141 | 142 | // content 143 | while (len--) { 144 | translation = translations[len]; 145 | str = ""; 146 | for (i = 0; i < lenLocales; i++) { 147 | str += delimiter + (translation[ locales[i] ] || ""); 148 | } 149 | res.write(translation.project.toString() 150 | + delimiter + translation.key 151 | + delimiter + (translation.description || "") 152 | + str + "\r\n"); 153 | } 154 | 155 | res.end(); 156 | }); 157 | }); 158 | 159 | export default router 160 | -------------------------------------------------------------------------------- /src/controllers/HistoryController.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import History from '../models/HistoryModel' 3 | 4 | const router = express.Router() 5 | 6 | const getHistoryByTranslationId = translationId => { 7 | return new Promise((resolve, reject) => { 8 | History.findOne({ translationId }, (err, history) => { 9 | if (err) { 10 | reject(err); 11 | } 12 | resolve(history); 13 | }); 14 | }); 15 | } 16 | 17 | router.route('/:translationId') 18 | .get((req, res) => { 19 | getHistoryByTranslationId(req.params.translationId) 20 | .then(history => { 21 | res.json(history); 22 | }) 23 | .catch(err => { 24 | res.status(500).send(err); 25 | }); 26 | }); 27 | 28 | export default router 29 | -------------------------------------------------------------------------------- /src/controllers/KeyController.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import mongoose from 'mongoose' 3 | import mergeUtil from 'keys-translations-manager-core/lib/mergeUtil' 4 | import Translations from '../models/TranslationModel' 5 | import History from '../models/HistoryModel' 6 | import config from '../../ktm.config' 7 | const locales = config.locales 8 | const router = express.Router() 9 | const findMergeable = mergeUtil.findMergeable 10 | let bulk, doc, bulkHistory, docHistory 11 | 12 | router.route('/') 13 | .get(function(req, res) { 14 | Translations.find(function(err, translations) { 15 | if (err) { 16 | res.status(500).send(err); 17 | } 18 | res.json(findMergeable(translations, locales)); 19 | }); 20 | }) 21 | .post(function(req, res) { 22 | const mergeable = req.body; 23 | let len = mergeable.length, 24 | l, 25 | translationAry, 26 | translation, 27 | projects; 28 | 29 | bulk = Translations.collection.initializeUnorderedBulkOp(); 30 | bulkHistory = History.collection.initializeUnorderedBulkOp(); 31 | 32 | while(len--){ 33 | translationAry = mergeable[len]; 34 | l = translationAry.length; 35 | projects = []; 36 | while(l--){ 37 | translation = translationAry[l]; 38 | projects = projects.concat(translation.project) 39 | doc = bulk.find({'_id': mongoose.Types.ObjectId(translation._id)}); 40 | if (l > 0) { // delete 41 | doc.remove(); 42 | } else { // update 43 | const log = { 44 | time: +new Date(), 45 | action: 'MERGE', 46 | // user: 'system', 47 | translation: { ...translation, project: projects }, 48 | }; 49 | docHistory = bulkHistory.find({ 50 | 'translationId': translation._id 51 | }); 52 | 53 | if (docHistory) { 54 | docHistory.update({ $push: { logs: log } }); 55 | } else { 56 | bulkHistory.insert({ 57 | translationId: translation._id, 58 | logs: [log] 59 | }); 60 | } 61 | doc.update({ $set: {project: projects} }); 62 | } 63 | } 64 | } 65 | 66 | bulkHistory.execute(); 67 | bulk.execute(function(){ 68 | Translations.find({}, null, {sort: {'_id': -1}}, function(err, translations) { 69 | if (err) { 70 | res.status(500).send(err); 71 | } 72 | res.json({ 73 | action: "m", 74 | success: true, 75 | data: translations, 76 | errors: [] 77 | }); 78 | }); 79 | }); 80 | }); 81 | 82 | export default router 83 | -------------------------------------------------------------------------------- /src/controllers/VisController.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import transformationUtil from 'keys-translations-manager-core/lib/transformationUtil' 3 | import Translations from '../models/TranslationModel' 4 | const router = express.Router() 5 | 6 | router.route('/:visType/:project') 7 | .get(function(req, res) { 8 | const visType = req.params.visType, 9 | project = req.params.project, 10 | criteria = { "project": project }; 11 | let query, 12 | len, 13 | translation; 14 | 15 | query = Translations.find(criteria).sort({'key':-1}); 16 | //Translations.find({ "project": project }, function(err, translations) { 17 | query.exec(function(err, translations) { 18 | if (err) { 19 | res.status(500).send(err); 20 | } 21 | 22 | if (visType === "tree") { 23 | let rootObj = {}; 24 | len = translations.length; 25 | while(len--) { 26 | translation = translations[len]; 27 | rootObj = transformationUtil.properties2Json(rootObj, translation.key, translation); 28 | } 29 | res.json( transformationUtil.json2Tree(rootObj) ); 30 | } else { 31 | res.json(translations); 32 | } 33 | }); 34 | }); 35 | 36 | export default router 37 | -------------------------------------------------------------------------------- /src/models/HistoryModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'), 2 | schema = { 3 | translationId: { 4 | type: String, 5 | required: true, 6 | unique: true, 7 | index: true, 8 | }, 9 | isDeleted: { 10 | type: Boolean, 11 | default: false, 12 | }, 13 | logs: [{ 14 | time: { 15 | type: Number, 16 | }, 17 | action: { 18 | type: String, 19 | enum: ['ADD', 'EDIT', 'DELETE', 'IMPORT', 'MERGE'], 20 | }, 21 | user: { 22 | type: String, 23 | trim: true, 24 | }, 25 | translation: { 26 | type: Object, 27 | } 28 | }], 29 | }, 30 | HistorySchema = new mongoose.Schema(schema); 31 | 32 | module.exports = mongoose.model('History', HistorySchema); 33 | -------------------------------------------------------------------------------- /src/models/TranslationModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var config = require('../../ktm.config'); 3 | 4 | var locales = config.locales, 5 | lenLocales = locales.length, 6 | schema = { 7 | 'key': String, 8 | 'description': String, 9 | 'project': [String] 10 | }, 11 | TranslationSchema; 12 | 13 | while(lenLocales--) { 14 | schema[locales[lenLocales]] = String; 15 | } 16 | TranslationSchema = new mongoose.Schema(schema); 17 | 18 | module.exports = mongoose.model('Translation', TranslationSchema); 19 | -------------------------------------------------------------------------------- /src/reducers/components.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes' 2 | import { INIT_COMPONENTS } from '../constants/InitStates' 3 | 4 | export default function components(state = INIT_COMPONENTS, action) { 5 | switch (action.type) { 6 | case ActionTypes.SHOW_MESSAGEPOPUP: 7 | return { 8 | ...state, 9 | showmessagepopup: true 10 | }; 11 | case ActionTypes.LOAD_TRANSLATIONS: 12 | case ActionTypes.LOAD_TREE_DATA: 13 | case ActionTypes.CLOSE_MESSAGEPOPUP: 14 | return { 15 | ...state, 16 | showmessagepopup: false, 17 | reloaddata: false 18 | }; 19 | case ActionTypes.SHOW_IMPORTMODAL: 20 | return { 21 | ...state, 22 | showimportmodal: true 23 | }; 24 | case ActionTypes.IMPORT_LOCALE: 25 | case ActionTypes.CLOSE_IMPORTMODAL: 26 | return { 27 | ...state, 28 | showimportmodal: false 29 | }; 30 | case ActionTypes.FIND_MERGEABLE: 31 | return { 32 | ...state, 33 | showmergemodal: true, 34 | keys: action.data.keys, 35 | mergeable: action.data.mergeable 36 | }; 37 | case ActionTypes.MERGE_TRANSLATIONS: 38 | case ActionTypes.CLOSE_MERGEMODAL: 39 | return { 40 | ...state, 41 | showmergemodal: false 42 | }; 43 | case ActionTypes.SHOW_EDITMODAL: 44 | return { 45 | ...state, 46 | showeditmodal: true, 47 | editrecord: action.record 48 | }; 49 | case ActionTypes.UPDATE_TRANSLATION: 50 | case ActionTypes.CLOSE_EDITMODAL: 51 | return { 52 | ...state, 53 | showeditmodal: false 54 | }; 55 | case ActionTypes.SHOW_HISTORYMODAL: 56 | return { 57 | ...state, 58 | showhistorymodal: true, 59 | editrecord: action.record, 60 | }; 61 | case ActionTypes.CLOSE_HISTORYMODAL: 62 | return { 63 | ...state, 64 | showhistorymodal: false 65 | }; 66 | case ActionTypes.SHOW_CONFIRMMODAL: 67 | return { 68 | ...state, 69 | showconfirmmodal: true, 70 | editrecord: action.record, 71 | }; 72 | case ActionTypes.REMOVE_TRANSLATION: 73 | case ActionTypes.CLOSE_CONFIRMMODAL: 74 | return { 75 | ...state, 76 | showconfirmmodal: false 77 | }; 78 | case ActionTypes.RELOAD_DATA: 79 | return { 80 | ...state, 81 | reloaddata: true 82 | }; 83 | case ActionTypes.SHOW_TOOLTIP: 84 | return { 85 | ...state, 86 | showtooltip: true, 87 | tooltiptop: action.top || 0, 88 | tooltipleft: action.left || 0 89 | }; 90 | case ActionTypes.HIDE_TOOLTIP: 91 | return { 92 | ...state, 93 | showtooltip: false 94 | }; 95 | default: 96 | return state; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/reducers/counts.js: -------------------------------------------------------------------------------- 1 | import { LOAD_COUNTS } from '../constants/ActionTypes' 2 | import { INIT_COUNTS } from '../constants/InitStates' 3 | 4 | export default function counts(state = INIT_COUNTS, action) { 5 | switch (action.type) { 6 | case LOAD_COUNTS: 7 | return action.counts 8 | default: 9 | return state; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/reducers/errors.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes' 2 | import {INIT_ERRORS} from '../constants/InitStates' 3 | 4 | export default function errors(state = INIT_ERRORS, action) { 5 | switch (action.type) { 6 | case ActionTypes.ALERT_ERRORS: 7 | return action.errors; 8 | case ActionTypes.LOAD_MESSAGES: 9 | case ActionTypes.LOAD_COUNTS: 10 | case ActionTypes.SHOW_EDITMODAL: 11 | case ActionTypes.SHOW_IMPORTMODAL: 12 | case ActionTypes.CLEAR_ERRORS: 13 | return []; 14 | default: 15 | return state; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/reducers/history.js: -------------------------------------------------------------------------------- 1 | import { SHOW_HISTORYMODAL, LOAD_HISTORY } from '../constants/ActionTypes' 2 | import { INIT_HISTORY } from '../constants/InitStates' 3 | import { STATUS_FETCHED } from '../constants/Status' 4 | 5 | export default function history(state = INIT_HISTORY, action) { 6 | switch (action.type) { 7 | case SHOW_HISTORYMODAL: 8 | case LOAD_HISTORY: 9 | return { 10 | historylog: action.historylog || [], 11 | historystatus: action.status || STATUS_FETCHED, 12 | }; 13 | default: 14 | return state; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import messages from './messages' 3 | import counts from './counts' 4 | import errors from './errors' 5 | import translations from './translations' 6 | import history from './history' 7 | import vis from './vis' 8 | import socket from './socket' 9 | import components from './components' 10 | 11 | const rootReducer = combineReducers({ 12 | messages, counts, errors, translations, history, vis, socket, components 13 | }) 14 | 15 | export default rootReducer 16 | -------------------------------------------------------------------------------- /src/reducers/messages.js: -------------------------------------------------------------------------------- 1 | import { LOAD_MESSAGES } from '../constants/ActionTypes' 2 | import { INIT_MESSAGES } from '../constants/InitStates' 3 | 4 | export default function messages(state = INIT_MESSAGES, action) { 5 | switch (action.type) { 6 | case LOAD_MESSAGES: 7 | return { 8 | lang: action.lang, 9 | messages: action.messages 10 | }; 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/reducers/socket.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes' 2 | import { INIT_SOCKET } from '../constants/InitStates' 3 | 4 | export default function socket(state = INIT_SOCKET, action) { 5 | switch (action.type) { 6 | case ActionTypes.ADD_TRANSLATION: 7 | case ActionTypes.REMOVE_TRANSLATION: 8 | case ActionTypes.UPDATE_TRANSLATION: 9 | case ActionTypes.IMPORT_LOCALE: 10 | return { 11 | ...state, 12 | emitdatachange: true 13 | }; 14 | case ActionTypes.END_DATACHANGE: 15 | return { 16 | ...state, 17 | emitdatachange: false 18 | }; 19 | default: 20 | return state; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/reducers/translations.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes' 2 | import { INIT_TRANSLATIONS } from '../constants/InitStates' 3 | 4 | export default function translations(state = INIT_TRANSLATIONS, action) { 5 | const getIndex = function(id) { 6 | return (state || []).map(function(e){ 7 | return e._id; 8 | }).indexOf(id); 9 | }; 10 | let index; 11 | 12 | switch (action.type) { 13 | case ActionTypes.ADD_TRANSLATION: 14 | state = state || []; //might be null 15 | return [action.data, ...state]; 16 | 17 | case ActionTypes.LOAD_TRANSLATIONS: 18 | case ActionTypes.IMPORT_LOCALE: 19 | case ActionTypes.MERGE_TRANSLATIONS: 20 | return action.data; 21 | 22 | case ActionTypes.REMOVE_TRANSLATION: 23 | state = state || []; //might be null 24 | index = getIndex(action.id); 25 | return [...state.slice(0, index), 26 | ...state.slice(index + 1)]; 27 | 28 | case ActionTypes.UPDATE_TRANSLATION: 29 | state = state || []; //might be null 30 | index = getIndex(action.data._id); 31 | return [...state.slice(0, index), 32 | action.data, 33 | ...state.slice(index + 1)]; 34 | 35 | default: 36 | return state; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/reducers/vis.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes' 2 | import { INIT_VIS } from '../constants/InitStates' 3 | 4 | export default function vis(state = INIT_VIS, action) { 5 | switch (action.type) { 6 | case ActionTypes.LOAD_TREE_DATA: 7 | return { 8 | ...state, 9 | treedata: action.data 10 | }; 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { renderToString } from 'react-dom/server' 3 | import { Provider } from 'react-redux' 4 | import { StaticRouter } from 'react-router' 5 | import configureStore from '../store/configureStore' 6 | import RootContainer from '../containers/RootContainer' 7 | 8 | export default function markup(initialState, req, context){ 9 | return renderToString( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import rootReducer from '../reducers' 4 | 5 | const createStoreWithMiddleware = compose( 6 | applyMiddleware(thunk), 7 | (typeof window !== "undefined" && window.devToolsExtension) 8 | ? window.devToolsExtension() 9 | : f => f 10 | )(createStore) 11 | 12 | export default function configureStore(initialState) { 13 | const store = createStoreWithMiddleware(rootReducer, initialState) 14 | 15 | if (module.hot) { 16 | module.hot.accept('../reducers', () => { 17 | const nextReducer = require('../reducers').default 18 | store.replaceReducer(nextReducer) 19 | }) 20 | } 21 | return store 22 | } 23 | -------------------------------------------------------------------------------- /tests/mock/translation: -------------------------------------------------------------------------------- 1 | { 2 | "ui": { 3 | "common": { 4 | "add": "Add", 5 | "delete": "Delete" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/mock/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "ui": { 3 | "common": { 4 | "add": "Add", 5 | "delete": "Delete" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/mock/translation.properties: -------------------------------------------------------------------------------- 1 | ui.common.add=Add 2 | ui.common.delete=Delete 3 | -------------------------------------------------------------------------------- /tests/packages/keys-translations-manager-core/lib/historyUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import historyUtil from "../../../../packages/keys-translations-manager-core/lib/historyUtil" 3 | 4 | describe('[utility] historyUtil', function() { 5 | describe('diff', function() { 6 | it("should return false if no arguments given", function() { 7 | expect(historyUtil.differentiate()).to.be.false; 8 | }); 9 | 10 | it("should find additions if new value is added", function() { 11 | const prevLog = { 12 | "project": ["p1", "p2"], 13 | "en-US": "Add", 14 | "key": "ui.commom.add", 15 | }, 16 | log = { 17 | "project": ["p1", "p2"], 18 | "en-US": "Add", 19 | "key": "ui.commom.add", 20 | "description": "new desc", 21 | }; 22 | 23 | expect(historyUtil.differentiate(prevLog, log)).to.deep.eql({ 24 | "project": { 25 | "original": ["p1", "p2"] 26 | }, 27 | "en-US": { 28 | "original": "Add" 29 | }, 30 | "key": { 31 | "original": "ui.commom.add" 32 | }, 33 | "description": { 34 | "addition": "new desc" 35 | } 36 | }); 37 | }); 38 | 39 | it("should have no difference if logs are the same", function() { 40 | const prevLog = { 41 | "project": ["p1", "p2"], 42 | "en-US": "Add", 43 | "key": "ui.commom.add", 44 | "description": "new desc", 45 | }, 46 | log = { 47 | "project": ["p2", "p1"], // different order is fine 48 | "en-US": "Add", 49 | "key": "ui.commom.add", 50 | "description": "new desc", 51 | }; 52 | 53 | expect(historyUtil.differentiate(prevLog, log)).to.deep.eql({ 54 | "project": { 55 | "original": ["p1", "p2"] 56 | }, 57 | "en-US": { 58 | "original": "Add" 59 | }, 60 | "key": { 61 | "original": "ui.commom.add" 62 | }, 63 | "description": { 64 | "original": "new desc" 65 | } 66 | }); 67 | }); 68 | 69 | it("should find additions and deletions if the value is modified", function() { 70 | const prevLog = { 71 | "project": ["p1", "p2"], 72 | "en-US": "Add", 73 | "key": "ui.commom.add", 74 | "description": "new desc", 75 | }, 76 | log = { 77 | "project": ["p2"], 78 | "en-US": "Add", 79 | "key": "ui.commom.add", 80 | "description": "updated desc", 81 | }; 82 | 83 | expect(historyUtil.differentiate(prevLog, log)).to.deep.eql({ 84 | "project": { 85 | "addition": ["p2"], 86 | "deletion": ["p1", "p2"], 87 | }, 88 | "en-US": { 89 | "original": "Add" 90 | }, 91 | "key": { 92 | "original": "ui.commom.add" 93 | }, 94 | "description": { 95 | "addition": "updated desc", 96 | "deletion": "new desc", 97 | } 98 | }); 99 | }); 100 | 101 | it("should find deletions if the value is removed", function() { 102 | const prevLog = { 103 | "project": ["p1", "p2"], 104 | "en-US": "Add", 105 | "key": "ui.commom.add", 106 | "description": "updated desc", 107 | }, 108 | log = { 109 | "project": ["p1", "p2"], 110 | "en-US": "Add", 111 | "key": "ui.commom.add", 112 | }; 113 | 114 | expect(historyUtil.differentiate(prevLog, log)).to.deep.eql({ 115 | "project": { 116 | "original": ["p1", "p2"] 117 | }, 118 | "en-US": { 119 | "original": "Add" 120 | }, 121 | "key": { 122 | "original": "ui.commom.add" 123 | }, 124 | "description": { 125 | "deletion": "updated desc" 126 | } 127 | }); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /tests/packages/keys-translations-manager-core/lib/importUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import path from 'path' 3 | import importUtil from "../../../../packages/keys-translations-manager-core/lib/importUtil" 4 | let fakeFile = path.join(__dirname, "fakeFile") 5 | let jsonFile = path.join(__dirname, "../../../mock/translation.json") 6 | let propertiesFile = path.join(__dirname, "../../../mock/translation.properties") 7 | let fileWithNoExt = path.join(__dirname, "../../../mock/translation") 8 | 9 | describe('[utility] importUtil', function() { 10 | describe('read', function() { 11 | it("should return err if no file found", function(done) { 12 | importUtil.read(fakeFile, function(err, data){ 13 | expect(err).to.be.an('error'); 14 | done(); 15 | }) 16 | }); 17 | 18 | it("should return parsed data if a JSON file was read", function(done) { 19 | importUtil.read(jsonFile, function(err, type, data) { 20 | expect(type).to.be.eql('json'); 21 | expect(data).to.deep.eql({ 22 | "ui": { 23 | "common": { 24 | "add": "Add", 25 | "delete": "Delete" 26 | } 27 | } 28 | }); 29 | done(); 30 | }) 31 | }); 32 | 33 | it("should return parsed data if a properties file was read", function(done) { 34 | importUtil.read(propertiesFile, function(err, type, data) { 35 | expect(type).to.be.eql('properties'); 36 | expect(data).to.deep.eql({ 37 | 'ui.common.add': 'Add', 38 | 'ui.common.delete': 'Delete' 39 | }); 40 | done(); 41 | }) 42 | }); 43 | 44 | it("should parse data even if the file has no filename extension", function(done) { 45 | importUtil.read(fileWithNoExt, function(err, type, data) { 46 | expect(type).to.be.eql('json'); 47 | expect(data).to.deep.eql({ 48 | "ui": { 49 | "common": { 50 | "add": "Add", 51 | "delete": "Delete" 52 | } 53 | } 54 | }); 55 | done(); 56 | }) 57 | }); 58 | }); 59 | 60 | describe('validate', function() { 61 | it("return errors if keys conflict", function() { 62 | var srcData = { 63 | "ui.common.add": "Add", 64 | "ui.common.delete": "Delete", 65 | "ui.file.type.json": "JSON", 66 | "ui.test": "Test" 67 | }, 68 | destData = [{ 69 | "project": ["p2"], 70 | "en-US": "ADD", 71 | "key": "ui.common.add" 72 | }, { 73 | "project": ["p2"], 74 | "en-US": "File Type", 75 | "key": "ui.file.type" 76 | }, { 77 | "project": ["p2"], 78 | "zh-TW": "測試...", 79 | "en-US": "Test Desc", 80 | "key": "ui.test.desc", 81 | "description": "qwe" 82 | }], 83 | error; 84 | 85 | error = importUtil.validate(srcData, destData); 86 | expect(error).to.deep.eql({ 87 | iequals: ['ui.common.add'], 88 | iconflicts: ['ui.test', 'ui.file.type.json'] 89 | }); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /tests/packages/keys-translations-manager-core/lib/localeUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import localeUtil from "../../../../packages/keys-translations-manager-core/lib/localeUtil" 3 | 4 | describe('[utility] localeUtil', function() { 5 | describe('setMessages', function() { 6 | before(function() { 7 | localeUtil.setMessages({ 8 | 'ui': { 9 | 'common': { 10 | 'add': 'Add' 11 | }, 12 | 'message': { 13 | 'unread': 'You have {0} unread messages.' 14 | } 15 | } 16 | }); 17 | }); 18 | 19 | it("should set messages", function() { 20 | expect(localeUtil.messages).to.deep.eql({ 21 | 'ui': { 22 | 'common': { 23 | 'add': 'Add' 24 | }, 25 | 'message': { 26 | 'unread': 'You have {0} unread messages.' 27 | } 28 | } 29 | }); 30 | }); 31 | }); 32 | 33 | describe('getMsg', function() { 34 | before(function() { 35 | localeUtil.setMessages({ 36 | 'ui': { 37 | 'common': { 38 | 'add': 'Add' 39 | }, 40 | 'message': { 41 | 'unread': 'You have {0} unread messages.' 42 | } 43 | } 44 | }); 45 | }); 46 | 47 | it("should return a value if a key exists", function() { 48 | expect(localeUtil.getMsg('ui.common.add')) 49 | .to.be.equal('Add'); 50 | }); 51 | 52 | it("should replace replaceholder", function() { 53 | expect(localeUtil.getMsg('ui.message.unread', 3)) 54 | .to.be.equal('You have 3 unread messages.'); 55 | }); 56 | 57 | it("should return `key`.undefined if a key doesn't exist", function() { 58 | expect(localeUtil.getMsg('ui.common.update')) 59 | .to.be.equal('ui.common.update.undefined'); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/packages/keys-translations-manager-core/lib/logUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import logUtil from "../../../../packages/keys-translations-manager-core/lib/logUtil" 3 | 4 | describe('[utility] logUtil', function() { 5 | describe('log', function() { 6 | beforeEach(function() { 7 | sinon.spy(console, 'log'); 8 | }); 9 | afterEach(function() { 10 | console.log.restore(); 11 | }); 12 | 13 | it("should log something with chalk", function() { 14 | logUtil.log('info', 'log something'); 15 | expect(console.log).calledOnce; 16 | 17 | logUtil.log('warn', 'log something'); 18 | expect(console.log).calledTwice; 19 | 20 | logUtil.log('error', 'log something'); 21 | expect(console.log).calledThrice; 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/packages/keys-translations-manager-core/lib/mergeUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import mergeUtil from "../../../../packages/keys-translations-manager-core/lib/mergeUtil" 3 | 4 | describe('[utility] mergeUtil', function() { 5 | describe('findMergeable', function() { 6 | it("should return initial value if translations or locales not given", function() { 7 | expect(mergeUtil.findMergeable()).to.deep.eql({ 8 | keys: {}, 9 | mergeable: [] 10 | }); 11 | }); 12 | 13 | it("should return keys{} and mergeable[] if mergeable records found", function() { 14 | var translations = [{ 15 | "project": ["p1", "p2"], 16 | "__v": 0, 17 | "zh-TW": "增加", 18 | "en-US": "Add", 19 | "key": "ui.commom.add", 20 | "description": "", 21 | "_id": "575cb114b92c33281f2ef6d7" 22 | }, { 23 | "project": ["p2"], 24 | "__v": 1, 25 | "zh-TW": "刪除", 26 | "en-US": "Delete", 27 | "key": "ui.common.delete", 28 | "description": "", 29 | "_id": "575cc74ba0efba240cbf53c3" 30 | }, { 31 | "project": ["p1"], 32 | "__v": 0, 33 | "zh-TW": "刪除", 34 | "en-US": "Delete", 35 | "key": "ui.common.delete", 36 | "description": "", 37 | "_id": "575cc79ca0efba240cbf53c4" 38 | }, { 39 | "project": ["p3"], 40 | "__v": 0, 41 | "zh-TW": "移除", 42 | "en-US": "Delete", 43 | "key": "ui.common.delete", 44 | "description": "", 45 | "_id": "575cc7a3a0efba240cbf53c5" 46 | }, { 47 | "project": ["p4"], 48 | "__v": 0, 49 | "zh-TW": "移除", 50 | "en-US": "Delete", 51 | "key": "ui.common.delete", 52 | "description": "", 53 | "_id": "575cc7a5a0efba240cbf53c6" 54 | }], 55 | locales = ["en-US", "zh-TW"]; 56 | 57 | expect(mergeUtil.findMergeable(translations, locales)).to.deep.eql({ 58 | "keys": { 59 | "ui.common.delete": true 60 | }, 61 | "mergeable": [ 62 | [{ 63 | "_id": "575cc7a5a0efba240cbf53c6", 64 | "description": "", 65 | "key": "ui.common.delete", 66 | "en-US": "Delete", 67 | "zh-TW": "移除", 68 | "__v": 0, 69 | "project": ["p4"] 70 | }, { 71 | "_id": "575cc7a3a0efba240cbf53c5", 72 | "description": "", 73 | "key": "ui.common.delete", 74 | "en-US": "Delete", 75 | "zh-TW": "移除", 76 | "__v": 0, 77 | "project": ["p3"] 78 | }], [{ 79 | "_id": "575cc79ca0efba240cbf53c4", 80 | "description": "", 81 | "key": "ui.common.delete", 82 | "en-US": "Delete", 83 | "zh-TW": "刪除", 84 | "__v": 0, 85 | "project": ["p1"] 86 | }, { 87 | "_id": "575cc74ba0efba240cbf53c3", 88 | "description": "", 89 | "key": "ui.common.delete", 90 | "en-US": "Delete", 91 | "zh-TW": "刪除", 92 | "__v": 1, 93 | "project": ["p2"] 94 | }] 95 | ] 96 | }); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /tests/packages/keys-translations-manager-core/lib/timingUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import timingUtil from "../../../../packages/keys-translations-manager-core/lib/timingUtil" 3 | 4 | describe('[utility] timingUtil', function() { 5 | describe('setTimeoutId', function() { 6 | before(function() { 7 | timingUtil.setTimeoutId(123); 8 | }); 9 | 10 | it("should set timeoutId", function() { 11 | expect(timingUtil.timeoutId).to.eql(123); 12 | }); 13 | }); 14 | 15 | describe('getTimeoutId', function() { 16 | before(function() { 17 | timingUtil.setTimeoutId(123); 18 | }); 19 | 20 | it("should return timeoutId", function() { 21 | expect(timingUtil.getTimeoutId()).to.be.equal(123); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/packages/keys-translations-manager-core/lib/transformationUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import transformationUtil from "../../../../packages/keys-translations-manager-core/lib/transformationUtil" 3 | 4 | let dataP, dataJ, dataD; 5 | 6 | describe('[utility] transformationUtil', function() { 7 | describe('properties2Json', function() { 8 | before(function() { 9 | dataP = [ 10 | ['ui.common.add', 'Add'], 11 | ['ui.message.unread', 'You have {0} unread messages.'] 12 | ]; 13 | }); 14 | 15 | it('should return JSON object', function() { 16 | var jsonObj = {}, 17 | len = dataP.length, 18 | i = 0, 19 | d; 20 | 21 | for (; i < len; i++) { 22 | d = dataP[i]; 23 | jsonObj = transformationUtil.properties2Json(jsonObj, d[0], d[1]); 24 | } 25 | 26 | expect(jsonObj).to.deep.equal({ 27 | ui: { 28 | common: { 29 | add: 'Add' 30 | }, 31 | message: { 32 | unread: 'You have {0} unread messages.' 33 | } 34 | } 35 | }); 36 | }); 37 | }); 38 | 39 | describe('json2Properties', function() { 40 | before(function() { 41 | dataJ = { 42 | "ui": { 43 | "common": { 44 | "add": "Add", 45 | "edit": "Edit" 46 | }, 47 | "json": { 48 | "format": "formatted", 49 | "mini": "minimized" 50 | } 51 | } 52 | }; 53 | }); 54 | 55 | it('should return Properties object', function() { 56 | var properties = {}; 57 | properties = transformationUtil.json2Properties(properties, dataJ, ""); 58 | expect(properties).to.deep.equal({ 59 | "ui.common.add": "Add", 60 | "ui.common.edit": "Edit", 61 | "ui.json.format": "formatted", 62 | "ui.json.mini": "minimized" 63 | }); 64 | }); 65 | }); 66 | 67 | describe('json2Tree', function() { 68 | before(function() { 69 | dataJ = { 70 | "ui": { 71 | "common": { 72 | "delete": { 73 | "description": "", 74 | "key": "ui.common.delete", 75 | "en-US": "Delete", 76 | "zh-TW": "刪除", 77 | "_id": "577a868da4d9538f0f7e4ef6", 78 | "__v": 0, 79 | "project": ["p1", "p2"] 80 | }, 81 | "add": { 82 | "description": "", 83 | "key": "ui.common.add", 84 | "en-US": "Add", 85 | "zh-TW": "新增", 86 | "_id": "577a8684a4d9538f0f7e4ef5", 87 | "__v": 0, 88 | "project": ["p1", "p2"] 89 | } 90 | } 91 | } 92 | }; 93 | }); 94 | 95 | it('should return tree data structure', function() { 96 | var tree = transformationUtil.json2Tree(dataJ); 97 | expect(tree).to.deep.equal([{ 98 | "name": "ui", 99 | "children": [{ 100 | "name": "common", 101 | "children": [{ 102 | "name": "delete", 103 | "translations": { 104 | "description": "", 105 | "key": "ui.common.delete", 106 | "en-US": "Delete", 107 | "zh-TW": "刪除", 108 | "_id": "577a868da4d9538f0f7e4ef6", 109 | "__v": 0, 110 | "project": ["p1", "p2"] 111 | } 112 | }, { 113 | "name": "add", 114 | "translations": { 115 | "description": "", 116 | "key": "ui.common.add", 117 | "en-US": "Add", 118 | "zh-TW": "新增", 119 | "_id": "577a8684a4d9538f0f7e4ef5", 120 | "__v": 0, 121 | "project": ["p1", "p2"] 122 | } 123 | }] 124 | }] 125 | }]); 126 | }); 127 | }); 128 | 129 | describe('document2FileContent', function() { 130 | before(function() { 131 | dataD = [ 132 | { 'key': 'ui.common.delete', 'en-US': 'Delete', 'zh-TW': '刪除' }, 133 | { 'key': 'ui.common.add', 'en-US': 'Add', 'zh-TW': '新增' } 134 | ]; 135 | }); 136 | 137 | it('should return string for nested JSON', function() { 138 | var str = transformationUtil.document2FileContent(dataD, 'zh-TW', 'json', false); 139 | expect(str).to.be.a('string'); 140 | expect(str).to.equal('{"ui":{"common":{"add":"新增","delete":"刪除"}}}'); 141 | }); 142 | 143 | it('should return string for flat JSON', function() { 144 | var str = transformationUtil.document2FileContent(dataD, 'en-US', 'flat', true); 145 | expect(str).to.be.a('string'); 146 | expect(str).to.equal(` 147 | { 148 | "ui.common.add": "Add", 149 | "ui.common.delete": "Delete" 150 | } 151 | `.trim()); 152 | }); 153 | 154 | it('should return string for Properties', function() { 155 | var str = transformationUtil.document2FileContent(dataD, 'en-US', 'properties', false); 156 | expect(str).to.be.a('string'); 157 | expect(str).to.equal('ui.common.add=Add\r\nui.common.delete=Delete\r\n'); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /tests/src/actions/components.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as actions from '../../../src/actions/components' 3 | 4 | const record = { 5 | "_id": "56e6509a7267ce4016109550", 6 | "en-US": "Add", 7 | "key": "ui.common.add", 8 | "project": ["p1", "p2"], 9 | "zh-TW": "新增" 10 | }; 11 | 12 | describe('(action) components', () => { 13 | describe('showEditModal', () => { 14 | it('should create an action to show EditModal', () => { 15 | expect(actions.showEditModal(record)) 16 | .to.deep.equal({ 17 | type: 'SHOW_EDITMODAL', 18 | record 19 | }) 20 | }) 21 | }) 22 | 23 | describe('closeEditModal', () => { 24 | it('should create an action to close EditModal', () => { 25 | expect(actions.closeEditModal()) 26 | .to.deep.equal({ 27 | type: 'CLOSE_EDITMODAL' 28 | }) 29 | }) 30 | }) 31 | 32 | describe('showConfirmModal', () => { 33 | it('should create an action to show ConfirmModal', () => { 34 | expect(actions.showConfirmModal(record)) 35 | .to.deep.equal({ 36 | type: 'SHOW_CONFIRMMODAL', 37 | record 38 | }) 39 | }) 40 | }) 41 | 42 | describe('closeConfirmModal', () => { 43 | it('should create an action to close ConfirmModal', () => { 44 | expect(actions.closeConfirmModal()) 45 | .to.deep.equal({ 46 | type: 'CLOSE_CONFIRMMODAL' 47 | }) 48 | }) 49 | }) 50 | 51 | describe('showHistoryModal', () => { 52 | it('should create an action to show HistoryModal', () => { 53 | expect(actions.showHistoryModal(record)) 54 | .to.deep.equal({ 55 | type: 'SHOW_HISTORYMODAL', 56 | record, 57 | }) 58 | }) 59 | }) 60 | 61 | describe('closeHistoryModal', () => { 62 | it('should create an action to close HistoryModal', () => { 63 | expect(actions.closeHistoryModal()) 64 | .to.deep.equal({ 65 | type: 'CLOSE_HISTORYMODAL' 66 | }) 67 | }) 68 | }) 69 | 70 | describe('closeMergeModal', () => { 71 | it('should create an action to close MergeModal', () => { 72 | expect(actions.closeMergeModal()) 73 | .to.deep.equal({ 74 | type: 'CLOSE_MERGEMODAL' 75 | }) 76 | }) 77 | }) 78 | 79 | describe('showImportModal', () => { 80 | it('should create an action to show ImportModal', () => { 81 | expect(actions.showImportModal()) 82 | .to.deep.equal({ 83 | type: 'SHOW_IMPORTMODAL' 84 | }) 85 | }) 86 | }) 87 | 88 | describe('closeImportModal', () => { 89 | it('should create an action to close ImportModal', () => { 90 | expect(actions.closeImportModal()) 91 | .to.deep.equal({ 92 | type: 'CLOSE_IMPORTMODAL' 93 | }) 94 | }) 95 | }) 96 | 97 | describe('showMessagePopup', () => { 98 | it('should create an action to show MessagePopup', () => { 99 | expect(actions.showMessagePopup()) 100 | .to.deep.equal({ 101 | type: 'SHOW_MESSAGEPOPUP' 102 | }) 103 | }) 104 | }) 105 | 106 | describe('closeMessagePopup', () => { 107 | it('should create an action to close MessagePopup', () => { 108 | expect(actions.closeMessagePopup()) 109 | .to.deep.equal({ 110 | type: 'CLOSE_MESSAGEPOPUP' 111 | }) 112 | }) 113 | }) 114 | 115 | describe('reloadData', () => { 116 | it('should create an action to reload data', () => { 117 | expect(actions.reloadData()) 118 | .to.deep.equal({ 119 | type: 'RELOAD_DATA' 120 | }) 121 | }) 122 | }) 123 | 124 | describe('showTooltip', () => { 125 | it('should create an action to show tooltip', () => { 126 | expect(actions.showTooltip(10, 20)) 127 | .to.deep.equal({ 128 | type: 'SHOW_TOOLTIP', 129 | top: 10, 130 | left: 20 131 | }) 132 | }) 133 | }) 134 | 135 | describe('hideTooltip', () => { 136 | it('should create an action to hide tooltip', () => { 137 | expect(actions.hideTooltip()) 138 | .to.deep.equal({ 139 | type: 'HIDE_TOOLTIP' 140 | }) 141 | }) 142 | }) 143 | }) 144 | -------------------------------------------------------------------------------- /tests/src/actions/counts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as actions from '../../../src/actions/counts' 3 | import { INIT_COUNTS } from '../../../src/constants/InitStates' 4 | 5 | describe('(action) counts', () => { 6 | describe('loadCounts', () => { 7 | afterEach(() => { 8 | nock.cleanAll() 9 | }) 10 | 11 | it("should create an action to count all the keys of each project", (done) => { 12 | nock(configUtil.getHost()) 13 | .filteringPath(/t=[^&]*/g, 't=123') 14 | .get('/api/count/projects?t=123') 15 | .reply(200, [{"_id":"p1","count":28}]) 16 | 17 | const store = mockStore(INIT_COUNTS) 18 | 19 | store.dispatch(actions.loadCounts()) 20 | .then(() => { 21 | expect(store.getActions()[0]) 22 | .to.deep.equal({ 23 | type: "LOAD_COUNTS", 24 | counts: { 25 | "p1": 28 26 | } 27 | }) 28 | }) 29 | .then(done) 30 | .catch(done) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /tests/src/actions/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as actions from '../../../src/actions/errors' 3 | 4 | const errors = []; 5 | 6 | describe('(action) errors', () => { 7 | describe('alertErrors', () => { 8 | it('should create an action to generate an error list', () => { 9 | expect(actions.alertErrors(errors)) 10 | .to.deep.equal({ 11 | type: 'ALERT_ERRORS', 12 | errors 13 | }) 14 | }) 15 | }) 16 | 17 | describe('clearErrors', () => { 18 | it('should create an action to clear error list', () => { 19 | expect(actions.clearErrors()) 20 | .to.deep.equal({ 21 | type: 'CLEAR_ERRORS', 22 | errors: [] 23 | }) 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /tests/src/actions/keys.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as actions from '../../../src/actions/keys' 3 | import { INIT_KEYS } from '../../../src/constants/InitStates' 4 | 5 | const data = { 6 | "keys":{ 7 | "ui.common.delete":true 8 | }, 9 | "mergeable":[ 10 | [{ 11 | "_id":"56e3f81b88cbd598067d7d60", 12 | "key":"ui.common.delete", 13 | "en-US":"Delete", 14 | "zh-TW":"移除", 15 | "__v":0, 16 | "project":["p2"] 17 | }, { 18 | "_id":"56e3f7fd88cbd598067d7d5e", 19 | "key":"ui.common.delete", 20 | "en-US":"Delete", 21 | "zh-TW":"移除", 22 | "__v":1, 23 | "project":["p1"] 24 | }] 25 | ] 26 | }; 27 | 28 | describe('(action) keys', () => { 29 | describe('findMergeable', () => { 30 | afterEach(() => { 31 | nock.cleanAll() 32 | }) 33 | 34 | it("should create an action to find mergeable keys", (done) => { 35 | nock(configUtil.getHost()) 36 | .filteringPath(/t=[^&]*/g, 't=123') 37 | .get('/api/key?t=123') 38 | .reply(200, data) 39 | 40 | const store = mockStore(INIT_KEYS) 41 | 42 | store.dispatch(actions.findMergeable()) 43 | .then(() => { 44 | expect(store.getActions()[0]) 45 | .to.deep.equal({ 46 | type: "FIND_MERGEABLE", 47 | data: data 48 | }) 49 | }) 50 | .then(done) 51 | .catch(done) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /tests/src/actions/messages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as actions from '../../../src/actions/messages' 3 | import { INIT_MESSAGES } from '../../../src/constants/InitStates' 4 | 5 | const lang = 'en-US' 6 | const messages = { 7 | 'ui': { 8 | 'common': { 9 | 'add': 'Add' 10 | }, 11 | 'message': { 12 | 'unread': 'You have {0} unread messages.' 13 | } 14 | } 15 | } 16 | 17 | describe('(action) messages', () => { 18 | describe('loadMessages', () => { 19 | afterEach(() => { 20 | nock.cleanAll() 21 | }) 22 | 23 | it("should create an action to load messages", (done) => { 24 | nock(configUtil.getHost()) 25 | .get('/public/locale/' + lang + '/translation.json') 26 | .reply(200, messages) 27 | 28 | const store = mockStore(INIT_MESSAGES) 29 | 30 | store.dispatch(actions.loadMessages(lang)) 31 | .then(() => { 32 | expect(store.getActions()[0]) 33 | .to.deep.equal({ 34 | type: "LOAD_MESSAGES", 35 | lang, 36 | messages 37 | }) 38 | }) 39 | .then(done) 40 | .catch(done) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /tests/src/actions/socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as actions from '../../../src/actions/socket' 3 | 4 | describe('(action) socket', () => { 5 | describe('endDataChange', () => { 6 | it('should create an action to update "datachange" flag', () => { 7 | expect(actions.endDataChange()) 8 | .to.deep.equal({ 9 | type: 'END_DATACHANGE' 10 | }) 11 | }) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/src/actions/vis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as actions from '../../../src/actions/vis' 3 | import { INIT_VIS } from '../../../src/constants/InitStates' 4 | 5 | const projectId = "p1", 6 | data = [{ 7 | "name": "ui", 8 | "children": [{ 9 | "name": "common", 10 | "children": [{ 11 | "name": "delete", 12 | "translations": { 13 | "description": "", 14 | "key": "ui.common.delete", 15 | "en-US": "Delete", 16 | "zh-TW": "刪除", 17 | "_id": "577a868da4d9538f0f7e4ef6", 18 | "__v": 0, 19 | "project": ["p1", "p2"] 20 | } 21 | }, { 22 | "name": "add", 23 | "translations": { 24 | "description": "", 25 | "key": "ui.common.add", 26 | "en-US": "Add", 27 | "zh-TW": "新增", 28 | "_id": "577a8684a4d9538f0f7e4ef5", 29 | "__v": 0, 30 | "project": ["p1", "p2"] 31 | } 32 | }] 33 | }] 34 | }]; 35 | 36 | describe('(action) vis', () => { 37 | describe('loadTreeData', () => { 38 | afterEach(() => { 39 | nock.cleanAll() 40 | }) 41 | 42 | it("should create an action to load data for tree layout", (done) => { 43 | nock(configUtil.getHost()) 44 | .filteringPath(/t=[^&]*/g, 't=123') 45 | .get(`/api/vis/tree/${projectId}?t=123`) 46 | .reply(200, data) 47 | 48 | const store = mockStore(INIT_VIS) 49 | 50 | store.dispatch(actions.loadTreeData(projectId)) 51 | .then(() => { 52 | expect(store.getActions()[0]) 53 | .to.deep.equal({ 54 | type: "LOAD_TREE_DATA", 55 | data: data 56 | }) 57 | }) 58 | .then(done) 59 | .catch(done) 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /tests/src/components/grid/ConfirmModal.js: -------------------------------------------------------------------------------- 1 | import ConfirmModal from '../../../../src/components/grid/ConfirmModal' 2 | import Modal from 'react-bootstrap/lib/Modal' 3 | 4 | function setup() { 5 | const props = { 6 | showconfirmmodal: false, 7 | data: {}, 8 | closeConfirmModal: sinon.spy(), 9 | removeTranslation: sinon.spy(), 10 | }, 11 | wrapper = shallow(); 12 | 13 | return { 14 | props, 15 | wrapper 16 | } 17 | } 18 | 19 | describe('(component) ConfirmModal', () => { 20 | it('should render as a ', () => { 21 | const { wrapper } = setup() 22 | expect(wrapper.type()).to.eql(Modal); 23 | }); 24 | 25 | it('should contain a "Yes" button with "primary" class', () => { 26 | const { wrapper } = setup() 27 | expect(wrapper.find('Button').first().props().bsStyle).to.eql('primary'); 28 | }); 29 | 30 | it('should contain a "No" button with "default" class', () => { 31 | const { wrapper } = setup() 32 | expect(wrapper.find('Button').last().props().bsStyle).to.eql('default'); 33 | }); 34 | 35 | it('should be opened if showconfirmmodal is set true', () => { 36 | const { wrapper } = setup() 37 | wrapper.setProps({ showconfirmmodal: true }); 38 | expect(wrapper.prop('show')).to.be.true; 39 | }); 40 | 41 | describe('child: "Yes" button', () => { 42 | it('should call state.confirmFunc() if clicked', () => { 43 | const { wrapper, props } = setup() 44 | wrapper.find('Button').first().simulate('click'); 45 | expect(props.removeTranslation).calledOnce; 46 | }); 47 | }); 48 | 49 | describe('child: "No" button', () => { 50 | it('should close Modal if clicked', () => { 51 | const { wrapper, props } = setup() 52 | wrapper.find('Button').last().simulate('click'); 53 | expect(props.closeConfirmModal).calledOnce; 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/src/components/grid/TablePanel.js: -------------------------------------------------------------------------------- 1 | import TablePanel from '../../../../src/components/grid/TablePanel' 2 | import Mask from '../../../../src/components/layout/Mask' 3 | 4 | function setup() { 5 | window.removeEventListener = sinon.spy() 6 | TablePanel.prototype.showEditModal = sinon.spy() 7 | 8 | const props = { 9 | translations: [{ 10 | "_id": "56d7037a0b70e760104ddf10", 11 | "en-US": "Edit", 12 | "key": "ui.common.edit", 13 | "project": ["p1"], 14 | "zh-TW": "編輯" 15 | }, { 16 | "_id": "56d7037a0b70e760104ddf11", 17 | "en-US": "Delete", 18 | "key": "ui.common.delete", 19 | "project": ["p1"], 20 | "zh-TW": "刪除" 21 | }], 22 | messages: {}, 23 | CountActions: { 24 | loadCounts: sinon.spy(), 25 | }, 26 | ComponentActions: { 27 | showEditModal: sinon.spy(), 28 | showConfirmModal: sinon.spy(), 29 | showHistoryModal: sinon.spy(), 30 | }, 31 | TranslationActions: { 32 | loadTranslations: sinon.spy(), 33 | updateTranslation: sinon.spy(), 34 | } 35 | }, 36 | wrapper = shallow(), 37 | wrapper2 = mount(); 38 | 39 | return { 40 | props, 41 | wrapper, 42 | wrapper2 43 | } 44 | } 45 | 46 | describe('(component) TablePanel', () => { 47 | it('should be wrapped by React.Fragment', () => { 48 | const { wrapper } = setup() 49 | expect(wrapper.type()).to.eql(React.Fragment); 50 | }); 51 | 52 | it('should have an InputGroup and a Mask', () => { 53 | const { wrapper } = setup() 54 | expect(wrapper.find('InputGroup')).to.have.length(1); 55 | expect(wrapper.find(Mask)).to.have.length(1); 56 | }); 57 | 58 | it('should have a Table with several columns', () => { 59 | const { wrapper } = setup(); 60 | expect(wrapper.find('ReactTable')).to.have.length(1); 61 | expect(wrapper.find('ReactTable').prop('columns').length) 62 | .to.eql(configUtil.getLocales().length + 4); 63 | }); 64 | 65 | it('should call loadTranslations() when component is mounted', () => { 66 | const { props } = setup(); 67 | props.TranslationActions.loadTranslations.resetHistory(); 68 | props.CountActions.loadCounts.resetHistory(); 69 | 70 | const wrapper = mount(); 71 | 72 | expect(props.TranslationActions.loadTranslations).calledOnce; 73 | 74 | wrapper.setProps({ reloaddata: true, translations: [] }); 75 | expect(props.TranslationActions.loadTranslations).calledTwice; 76 | expect(props.CountActions.loadCounts).calledOnce; 77 | }); 78 | 79 | it('should remove listener when the component is about to unmount', () => { 80 | const { wrapper } = setup() 81 | wrapper.unmount(); 82 | expect(window.removeEventListener).calledOnce; 83 | }); 84 | 85 | describe('child: InputGroup', () => { 86 | it('should call onQuickFilterText() if input value changed', done => { 87 | const { wrapper } = setup() 88 | const inputValue = 'test' 89 | wrapper.find('InputGroup').find('FormControl').first().simulate('change',{ target: { value: inputValue } }); 90 | 91 | setTimeout(() => { 92 | expect(wrapper.state('quickFilterText')).to.eql(inputValue); 93 | done(); 94 | }, 300); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /tests/src/components/import/ImportModal.js: -------------------------------------------------------------------------------- 1 | import ImportModal from '../../../../src/components/import/ImportModal' 2 | import Modal from 'react-bootstrap/lib/Modal' 3 | import Dropzone from 'react-dropzone' 4 | 5 | function setup() { 6 | const props = { 7 | showimportmodal: true, 8 | closeImportModal: sinon.spy(), 9 | errors: [], 10 | importLocale: sinon.spy(), 11 | alertErrors: sinon.spy(), 12 | clearErrors: sinon.spy() 13 | }, 14 | wrapper = shallow(); 15 | 16 | return { 17 | props, 18 | wrapper 19 | } 20 | } 21 | 22 | describe('(component) ImportModal', () => { 23 | it('should render as a ', () => { 24 | const { wrapper } = setup() 25 | expect(wrapper.type()).to.eql(Modal); 26 | }); 27 | 28 | it('should contain ', () => { 29 | const { wrapper } = setup() 30 | expect(wrapper.find('[clearErrors]')).to.have.length(1); 31 | }); 32 | 33 | it('should contain ', () => { 34 | const { wrapper } = setup() 35 | expect(wrapper.find(Dropzone)).to.have.length(1); 36 | }); 37 | 38 | it('should have 2 radio groups', () => { 39 | const { wrapper } = setup() 40 | expect(wrapper.find('.app-radio-group')).to.have.length(2); 41 | }); 42 | 43 | it('should contain a Confirm button with "primary" class', () => { 44 | const { wrapper } = setup() 45 | expect(wrapper.find('Button').first().props().bsStyle).to.eql('primary'); 46 | }); 47 | 48 | it('should contain a Dismiss button with "default" class', () => { 49 | const { wrapper } = setup() 50 | expect(wrapper.find('Button').last().props().bsStyle).to.eql('default'); 51 | }); 52 | 53 | describe('child: Dropzone', () => { 54 | it('should contain "ul" if there is no selected file', () => { 55 | const { props } = setup(); 56 | const wrapper = mount(); 57 | expect(wrapper.find('ul')).to.have.length(1); 58 | wrapper.setState({ selectedFile: { name: 'bar' } }); 59 | expect(wrapper.find('ul')).to.have.length(0); 60 | }); 61 | }); 62 | 63 | describe('child: "locale" radioGroup', () => { 64 | it('should call setLocale() if the selected changed', () => { 65 | const { wrapper } = setup() 66 | wrapper.find('Radio[name="locale"]').first().simulate('change'); 67 | expect(wrapper.state('selectedLocale')).to.not.be.undefined; 68 | }); 69 | }); 70 | 71 | describe('child: "project" radioGroup', () => { 72 | it('should call setProject() if the selected changed', () => { 73 | const { wrapper } = setup() 74 | wrapper.find('Radio[name="project"]').last().simulate('change'); 75 | expect(wrapper.state('selectedProject')).to.not.be.undefined; 76 | }); 77 | }); 78 | 79 | describe('child: confirm button', () => { 80 | it('should call either alertErrors() or importLocale() if clicked', () => { 81 | const { props, wrapper } = setup() 82 | wrapper.find('Button').first().simulate('click'); 83 | expect(props.alertErrors).calledOnce; 84 | 85 | wrapper.setState({ 86 | selectedFile: {}, 87 | selectedLocale: "en_US", 88 | selectedProject: "p1" 89 | }); 90 | wrapper.find('Button').first().simulate('click'); 91 | expect(props.importLocale).calledOnce; 92 | }); 93 | }); 94 | 95 | describe('child: dismiss button', () => { 96 | it('should call props.closeImportModal() if clicked', () => { 97 | const { props, wrapper } = setup() 98 | wrapper.find('Button').last().simulate('click'); 99 | expect(props.closeImportModal).calledOnce; 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /tests/src/components/input/AlertPanel.js: -------------------------------------------------------------------------------- 1 | import AlertPanel from '../../../../src/components/input/AlertPanel' 2 | 3 | function setup() { 4 | const props = { 5 | action: "c", 6 | clearErrors: sinon.spy(), 7 | errors: [{ 8 | action: "c", 9 | key: "ui.common.add", 10 | match: ["p1"], 11 | origin: null, 12 | params: { 13 | "en-US": "Add", 14 | "key": "ui.common.add", 15 | "project": ["p1"], 16 | "zh-TW": "新增" 17 | }, 18 | type: "equals" 19 | }] 20 | }, 21 | wrapper = shallow(); 22 | 23 | return { 24 | props, 25 | wrapper 26 | } 27 | } 28 | 29 | describe('(component) AlertPanel', () => { 30 | it('should render as a with "danger" class', () => { 31 | const { wrapper } = setup() 32 | expect(wrapper.props().bsClass).to.eql('alert'); 33 | expect(wrapper.props().bsStyle).to.eql('danger'); 34 | }); 35 | 36 | it('should call clearErrors() while dismiss', () => { 37 | const { props, wrapper } = setup() 38 | wrapper.props().onDismiss(); 39 | expect(props.clearErrors).calledOnce; 40 | }); 41 | 42 | it('should show error messages', () => { 43 | const { props, wrapper } = setup() 44 | expect(wrapper.find('p')).to.have.length(props.errors.length); 45 | }); 46 | 47 | it('should include
if the errors caused by creation', () => { 48 | const props = { 49 | action: "c", 50 | clearErrors: sinon.spy(), 51 | errors: [] 52 | } 53 | const wrapper = shallow() 54 | expect(wrapper.find('br')).to.have.length(1); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/src/components/input/EditModal.js: -------------------------------------------------------------------------------- 1 | import EditModal from '../../../../src/components/input/EditModal' 2 | import Modal from 'react-bootstrap/lib/Modal' 3 | 4 | function setup() { 5 | EditModal.prototype.updateTranslation = sinon.spy() 6 | let props = { 7 | showeditmodal: true, 8 | closeEditModal: sinon.spy(), 9 | data: { 10 | "_id": "56d7037a0b70e760104ddf10", 11 | "en-US": "Edit", 12 | "key": "ui.common.edit", 13 | "project": ["p1"], 14 | "zh-TW": "編輯" 15 | }, 16 | errors: [], 17 | updateTranslation: sinon.spy(), 18 | alertErrors: sinon.spy(), 19 | clearErrors: sinon.spy() 20 | }, 21 | wrapper = shallow(); 22 | 23 | return { 24 | props, 25 | wrapper 26 | } 27 | } 28 | 29 | describe('(component) EditModal', () => { 30 | it('should render as a ', () => { 31 | const { wrapper } = setup() 32 | expect(wrapper.type()).to.eql(Modal); 33 | }); 34 | 35 | it('should contain ', () => { 36 | const { wrapper } = setup() 37 | console.log(wrapper.debug()) 38 | expect(wrapper.find('[clearErrors]')).to.have.length(1); 39 | }); 40 | 41 | it('should contain ', () => { 42 | const { wrapper } = setup() 43 | expect(wrapper.find('FormPanel')).to.have.length(1); 44 | }); 45 | 46 | it('should contain a Confirm button with "primary" class', () => { 47 | const { wrapper } = setup() 48 | expect(wrapper.find('Button').first().props().bsStyle).to.eql('primary'); 49 | }); 50 | 51 | it('should contain a Dismiss button with "default" class', () => { 52 | const { wrapper } = setup() 53 | expect(wrapper.find('Button').last().props().bsStyle).to.eql('default'); 54 | }); 55 | 56 | describe('child: confirm button', () => { 57 | it('should call updateTranslation() if clicked', () => { 58 | const { props, wrapper } = setup() 59 | wrapper.find('Button').first().simulate('click'); 60 | expect(EditModal.prototype.updateTranslation).calledOnce; 61 | }); 62 | }); 63 | 64 | describe('child: dismiss button', () => { 65 | it('should call props.closeEditModal() if clicked', () => { 66 | const { props, wrapper } = setup() 67 | wrapper.find('Button').last().simulate('click'); 68 | expect(props.closeEditModal).calledOnce; 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /tests/src/components/input/FormPanel.js: -------------------------------------------------------------------------------- 1 | import FormPanel from '../../../../src/components/input/FormPanel' 2 | 3 | function setup() { 4 | const props = {} 5 | return { 6 | props, 7 | } 8 | } 9 | 10 | describe('(component) FormPanel', () => { 11 | it('should render as a
', () => { 12 | const { props } = setup() 13 | const wrapper = shallow() 14 | expect(wrapper.type()).to.eql('form'); 15 | }); 16 | 17 | it('should contain TextFields and checkboxes', () => { 18 | const { props } = setup() 19 | const wrapper = shallow() 20 | expect(wrapper.find('[required]')).to.have.length(configUtil.getLocales().length + 1); 21 | expect(wrapper.find('[componentClass="textarea"]')).to.have.length(1); 22 | expect(wrapper.find('Checkbox')).to.have.length(configUtil.getProjects().length); 23 | }); 24 | 25 | it('should have ".app-checkbox-options" class in create mode', () => { 26 | const { props } = setup() 27 | const wrapper = shallow() 28 | expect(wrapper.find(".app-checkbox-options")).to.have.length(1); 29 | }); 30 | 31 | it("should have no value set in create mode", () => { 32 | const { props } = setup() 33 | const wrapper = shallow() 34 | expect(wrapper.find('[name="key"]').prop("value")).to.be.undefined; 35 | expect(wrapper.find('[name="en-US"]').prop("defaultValue")).to.be.empty; 36 | expect(wrapper.find('[name="zh-TW"]').prop("defaultValue")).to.be.empty; 37 | expect(wrapper.find('[name="description"]').prop("defaultValue")).to.be.undefined; 38 | expect(wrapper.find('Checkbox[value="p1"]').prop("checked")).to.be.false; 39 | }); 40 | 41 | // it("should have readonly 'key' field in edit mode", () => { 42 | // const data = { 43 | // "_id": "56d7037a0b70e760104ddf10", 44 | // "description": "some description", 45 | // "en-US": "Edit", 46 | // "key": "ui.common.edit", 47 | // "project": ["p2"], 48 | // "zh-TW": "編輯" 49 | // } 50 | // const wrapper = shallow() 51 | // expect(wrapper.find('TextField[name="key"]').prop("readOnly")).to.be.true; 52 | // }); 53 | 54 | it("should have values set in edit mode", () => { 55 | const data = { 56 | "_id": "56d7037a0b70e760104ddf10", 57 | "description": "some description", 58 | "en-US": "Edit", 59 | "key": "ui.common.edit", 60 | "project": ["p2"], 61 | "zh-TW": "編輯" 62 | } 63 | const wrapper = shallow() 64 | expect(wrapper.find('[name="key"]').prop("defaultValue")).to.be.eql("ui.common.edit"); 65 | expect(wrapper.find('[name="en-US"]').prop("defaultValue")).to.be.eql("Edit"); 66 | expect(wrapper.find('[name="zh-TW"]').prop("defaultValue")).to.be.eql("編輯"); 67 | expect(wrapper.find('[name="description"]').prop("defaultValue")).to.be.eql("some description"); 68 | expect(wrapper.find('Checkbox[value="p1"]').prop("checked")).to.be.false; 69 | }); 70 | 71 | describe('child: input[type="checkbox"]', () => { 72 | it('should call onCheckboxChange() if checked/unchecked', () => { 73 | FormPanel.prototype.onCheckboxChange = sinon.spy() 74 | const wrapper = mount() 75 | wrapper.find('input[type="checkbox"]').last().simulate('change',{ target: { checked: false } }); 76 | expect(FormPanel.prototype.onCheckboxChange).calledOnce; 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /tests/src/components/input/InputPanel.js: -------------------------------------------------------------------------------- 1 | import InputPanel from '../../../../src/components/input/InputPanel' 2 | 3 | function setup() { 4 | const props = { 5 | messages: {}, 6 | addTranslation: sinon.spy(), 7 | alertErrors: sinon.spy() 8 | }, 9 | wrapper = shallow(); 10 | 11 | return { 12 | props, 13 | wrapper 14 | } 15 | } 16 | 17 | describe('(component) InputPanel', () => { 18 | it('should be wrapped by React.Fragment', () => { 19 | const { wrapper } = setup() 20 | expect(wrapper.type()).to.eql(React.Fragment); 21 | }); 22 | 23 | it('should contain an Add button', () => { 24 | const { wrapper } = setup() 25 | expect(wrapper.find('Button').find('i').hasClass('fa-plus-circle')).to.eql(true); 26 | }); 27 | 28 | describe('child: Button', () => { 29 | it('should call props.alertErrors() if required fields are empty', () => { 30 | const { props } = setup() 31 | const wrapper = mount(); 32 | wrapper.find('Button').simulate('click'); 33 | expect(props.alertErrors).calledOnce; 34 | expect(props.addTranslation.callCount).to.eql(0); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/src/components/input/TextField.js: -------------------------------------------------------------------------------- 1 | import TextField from '../../../../src/components/input/TextField' 2 | import FormGroup from 'react-bootstrap/lib/FormGroup' 3 | 4 | function setup() { 5 | const props = { 6 | name: "key", 7 | label: "Key" 8 | } 9 | 10 | return props 11 | } 12 | 13 | describe('(component) TextField', () => { 14 | it('should render as a ', () => { 15 | const wrapper = shallow( 16 | 17 | ) 18 | expect(wrapper.type()).to.eql(FormGroup); 19 | }); 20 | 21 | describe('child: ControlLabel', () => { 22 | it("shouldn't have ControlLabel if 'label' prop is not set", () => { 23 | const label = "Key" 24 | const wrapper = mount( 25 | 26 | ) 27 | expect(wrapper.find('ControlLabel')).to.have.length(0); 28 | }); 29 | 30 | it("should have a ControlLabel if 'label' prop is set", () => { 31 | const label = "Key" 32 | const wrapper = mount( 33 | 34 | ) 35 | expect(wrapper.find('ControlLabel').text()).to.be.eql(label + ":"); 36 | }); 37 | 38 | it("should display asterisk (*) to indicate required field", () => { 39 | const wrapper = mount( 40 | 41 | ) 42 | expect(wrapper.find('ControlLabel').text().substr(0, 1)).to.be.eql("*"); 43 | }); 44 | }); 45 | 46 | describe('child: FormControl', () => { 47 | it("shouldn't have style if 'value' prop is not set", () => { 48 | const wrapper = mount( 49 | 50 | ) 51 | expect(wrapper.find('FormControl').prop('style')).to.be.empty; 52 | }); 53 | 54 | it("should have a grey background if 'value' prop is set", () => { 55 | const wrapper = mount( 56 | 57 | ) 58 | expect(wrapper.find('FormControl').prop('style')).to.be.eql( 59 | {backgroundColor: "#e7e7e7"} 60 | ); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /tests/src/components/layout/DropdownMenu.js: -------------------------------------------------------------------------------- 1 | import DropdownMenu from '../../../../src/components/layout/DropdownMenu' 2 | 3 | function setup() { 4 | const props = { 5 | lang: 'en-US', 6 | loadMessages: sinon.spy(), 7 | showImportModal: sinon.spy(), 8 | findMergeable: sinon.spy() 9 | }, 10 | wrapper = shallow(); 11 | 12 | return { 13 | props, 14 | wrapper 15 | } 16 | } 17 | 18 | describe('(component) DropdownMenu', () => { 19 | it('should render as a
    ', () => { 20 | const { wrapper } = setup() 21 | expect(wrapper.type()).to.eql('ul'); 22 | }); 23 | 24 | describe('child: Import', () => { 25 | it('should call this.props.showImportModal() if clicked', () => { 26 | const { wrapper } = setup() 27 | wrapper.find('.nav').find('a').get(0).props.onClick(); 28 | }); 29 | }); 30 | 31 | describe('child: Merge', () => { 32 | it('should call this.props.findMergeable() if clicked', () => { 33 | const { props, wrapper } = setup() 34 | wrapper.find('.nav').find('a').get(1).props.onClick(); 35 | expect(props.findMergeable).calledOnce; 36 | }); 37 | }); 38 | 39 | describe('child: Language', () => { 40 | describe('if param equals this.props.lang', () => { 41 | it('shouldn\'t call this.props.loadMessages()', () => { 42 | const { props, wrapper } = setup() 43 | wrapper.find('.dropdown-menu').find('a').first().simulate('click'); 44 | expect(props.loadMessages).to.have.not.been.called; 45 | }); 46 | }); 47 | 48 | describe('if param is not equal to this.props.lang', () => { 49 | it('should call this.props.loadMessages()', () => { 50 | const { props, wrapper } = setup() 51 | wrapper.find('.dropdown-menu').find('a').get(1).props.onClick(); 52 | expect(props.loadMessages).calledOnce; 53 | 54 | wrapper.find('.dropdown-menu').find('a').get(2).props.onClick(); 55 | expect(props.loadMessages).calledTwice; 56 | }); 57 | }); 58 | }); 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /tests/src/components/layout/Header.js: -------------------------------------------------------------------------------- 1 | import Header from '../../../../src/components/layout/Header' 2 | 3 | function setup() { 4 | const wrapper = shallow(
    ) 5 | return wrapper 6 | } 7 | 8 | describe('(component) Header', () => { 9 | it('should render as a
    with class "navbar-header"', () => { 10 | const wrapper = setup() 11 | expect(wrapper.type()).to.eql('div'); 12 | expect(wrapper.hasClass("navbar-header")).to.be.true; 13 | }); 14 | 15 | describe('child: a', () => { 16 | it('should have "navbar-brand" class', () => { 17 | const wrapper = setup() 18 | expect(wrapper.find("a").hasClass("navbar-brand")).to.be.true; 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/src/components/layout/MainPanel.js: -------------------------------------------------------------------------------- 1 | import MainPanel from '../../../../src/components/layout/MainPanel' 2 | 3 | function setup() { 4 | const wrapper = render(test) 5 | return wrapper 6 | } 7 | 8 | describe('(component) MainPanel', () => { 9 | it('should render as a
    with class "row"', () => { 10 | const wrapper = setup()[0] 11 | expect(wrapper.name).to.eql('div'); 12 | expect(wrapper.attribs.class).to.eql('row'); 13 | }); 14 | 15 | describe('child: Col', () => { 16 | it('should have "col-lg-12" class', () => { 17 | const wrapper = setup() 18 | const child = wrapper.children()[0] 19 | expect(child.attribs.class).to.eql('col-lg-12'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/src/components/layout/Mask.js: -------------------------------------------------------------------------------- 1 | import Mask from '../../../../src/components/layout/Mask' 2 | import Spinner from '../../../../src/components/layout/Spinner' 3 | import Modal from 'react-bootstrap/lib/Modal' 4 | 5 | function setup() { 6 | const props = { 7 | show: false 8 | }, 9 | wrapper = shallow( 10 | 11 | clild 12 | 13 | ); 14 | 15 | return { 16 | props, 17 | wrapper 18 | } 19 | } 20 | 21 | describe('(component) Mask', () => { 22 | it('should render as a ', () => { 23 | const { wrapper } = setup() 24 | expect(wrapper.type()).to.eql(Modal); 25 | expect(wrapper.prop('backdrop')).to.eql('static'); 26 | expect(wrapper.prop('keyboard')).to.be.false; 27 | }); 28 | 29 | it('should have one icon font', () => { 30 | const { wrapper } = setup() 31 | expect(wrapper.find(Spinner)).to.have.length(1); 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /tests/src/components/layout/MessagePopup.js: -------------------------------------------------------------------------------- 1 | import MessagePopup from '../../../../src/components/layout/MessagePopup' 2 | 3 | function setup() { 4 | const props = { 5 | msg: 'Test Message', 6 | showmessagepopup: true, 7 | closeMessagePopup: sinon.spy() 8 | }, 9 | wrapper = shallow( 10 | 11 | clild 12 | 13 | ); 14 | 15 | return { 16 | props, 17 | wrapper 18 | } 19 | } 20 | 21 | describe('(component) MessagePopup', () => { 22 | it('should not display if showmessagepopup is false', () => { 23 | const { wrapper } = setup() 24 | wrapper.setProps({ showmessagepopup: false }); 25 | expect(wrapper.prop('style')).to.eql({ display: 'none' }); 26 | }); 27 | 28 | it('should render as a
    ', () => { 29 | const { wrapper } = setup() 30 | expect(wrapper.type()).to.eql('div'); 31 | expect(wrapper.prop('className')).to.eql('app-message-popup'); 32 | }); 33 | 34 | it('should have a child with "app-message-bar" class', () => { 35 | const { wrapper } = setup() 36 | expect(wrapper.childAt(0).prop('className')).to.eql('app-message-bar'); 37 | }); 38 | 39 | it('should show message and contain child nodes', () => { 40 | const { wrapper } = setup() 41 | expect(wrapper.children('.app-message-bar').contains("Test Message")).to.be.true; 42 | expect(wrapper.children('.app-message-bar').contains(clild)).to.be.true; 43 | }); 44 | 45 | it('should call this.props.closeMessagePopup() if the close icon clicked', () => { 46 | const { props, wrapper } = setup() 47 | wrapper.find('i').get(0).props.onClick(); 48 | expect(props.closeMessagePopup).calledOnce; 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /tests/src/components/layout/SideBar.js: -------------------------------------------------------------------------------- 1 | import Sidebar from '../../../../src/components/layout/SideBar' 2 | 3 | function setup() { 4 | const wrapper = shallow(test) 5 | return wrapper 6 | } 7 | 8 | describe('(component) SideBar', () => { 9 | it('should render as a
    ', () => { 10 | const wrapper = setup() 11 | expect(wrapper.type()).to.eql('div'); 12 | }); 13 | 14 | describe('child: li', () => { 15 | it('should have "sidebar-search" class', () => { 16 | const wrapper = setup() 17 | expect(wrapper.find("li").hasClass("sidebar-search")).to.be.true; 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/src/components/layout/Spinner.js: -------------------------------------------------------------------------------- 1 | import Spinner from '../../../../src/components/layout/Spinner' 2 | 3 | function setup() { 4 | const wrapper = shallow() 5 | return wrapper 6 | } 7 | 8 | describe('(component) Spinner', () => { 9 | it('should render as a styled
    ', () => { 10 | const wrapper = setup() 11 | expect(wrapper.type()).to.eql('div'); 12 | expect(wrapper.prop('style')).to.have.property('textAlign'); 13 | }); 14 | 15 | describe('child: i', () => { 16 | it('should have an icon font', () => { 17 | const wrapper = setup() 18 | expect(wrapper.find("i").hasClass("fa-spinner")).to.be.true; 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/src/components/merge/MergeModal.js: -------------------------------------------------------------------------------- 1 | import ImportModal from '../../../../src/components/merge/MergeModal' 2 | import Modal from 'react-bootstrap/lib/Modal' 3 | 4 | function setup() { 5 | const propsN = { 6 | keys: {}, 7 | mergeable: [], 8 | showmergemodal: true, 9 | closeMergeModal: sinon.spy(), 10 | mergeTranslations: sinon.spy() 11 | }, 12 | propsY = { 13 | keys: { 14 | "ui.common.delete": true 15 | }, 16 | mergeable: [ 17 | [{ 18 | "_id": "56e3f81b88cbd598067d7d60", 19 | "key": "ui.common.delete", 20 | "en-US": "Delete", 21 | "zh-TW": "移除", 22 | "__v": 0, 23 | "project": ["p2"] 24 | }, { 25 | "_id": "56e3f7fd88cbd598067d7d5e", 26 | "key": "ui.common.delete", 27 | "en-US": "Delete", 28 | "zh-TW": "移除", 29 | "__v": 1, 30 | "project": ["p1"] 31 | }] 32 | ], 33 | showmergemodal: true, 34 | closeMergeModal: sinon.spy(), 35 | mergeTranslations: sinon.spy() 36 | }, 37 | wrapperN = shallow(), 38 | wrapperY = shallow(); 39 | 40 | return { 41 | propsN, 42 | wrapperN, 43 | propsY, 44 | wrapperY 45 | } 46 | } 47 | 48 | describe('(component) MergeModal', () => { 49 | it('should render as a ', () => { 50 | const { wrapperY } = setup() 51 | expect(wrapperY.type()).to.eql(Modal); 52 | }); 53 | 54 | describe('with no keys', () => { 55 | it('should have no
    s', () => { 56 | const { wrapperN } = setup() 57 | expect(wrapperN.find('div')).to.have.length(0); 58 | }); 59 | 60 | it('should have 1 button', () => { 61 | const { wrapperN } = setup() 62 | expect(wrapperN.find('Button')).to.have.length(1); 63 | }); 64 | 65 | describe('child: close button', () => { 66 | it('should call props.closeMergeModal() if clicked', () => { 67 | const { propsN, wrapperN } = setup() 68 | wrapperN.find('Button').last().simulate('click'); 69 | expect(propsN.closeMergeModal).calledOnce; 70 | }); 71 | }); 72 | }); 73 | 74 | describe('with keys', () => { 75 | it('should contain a
    ', () => { 76 | const { wrapperY } = setup() 77 | expect(wrapperY.find('div')).to.have.length(1); 78 | }); 79 | 80 | it('should have 2 buttons', () => { 81 | const { wrapperY } = setup() 82 | expect(wrapperY.find('Button')).to.have.length(2); 83 | }); 84 | 85 | describe('child: confirm button', () => { 86 | it('should call props.mergeTranslations() if clicked', () => { 87 | const { propsY, wrapperY } = setup() 88 | wrapperY.find('Button').first().simulate('click'); 89 | expect(propsY.mergeTranslations).calledOnce; 90 | }); 91 | }); 92 | 93 | describe('child: dismiss button', () => { 94 | it('should call props.closeMergeModal() if clicked', () => { 95 | const { propsY, wrapperY } = setup() 96 | wrapperY.find('Button').last().simulate('click'); 97 | expect(propsY.closeMergeModal).calledOnce; 98 | }); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /tests/src/components/output/CountCol.js: -------------------------------------------------------------------------------- 1 | import CountCol from '../../../../src/components/output/CountCol' 2 | import Col from 'react-bootstrap/lib/Col' 3 | import { Link } from 'react-router-dom' 4 | 5 | function setup() { 6 | const props = { 7 | projectId: 'p1', 8 | header: 'header', 9 | onClick: sinon.spy(), 10 | count: 5, 11 | desc: 'description' 12 | }, 13 | wrapper = shallow(); 14 | 15 | return { 16 | props, 17 | wrapper 18 | } 19 | } 20 | 21 | describe('(component) CountCol', () => { 22 | it('should render as a ', () => { 23 | const { wrapper } = setup() 24 | expect(wrapper.type()).to.eql(Col); 25 | }); 26 | 27 | describe('child: heading', () => { 28 | it('should render with .panel-heading', () => { 29 | const { wrapper } = setup() 30 | expect(wrapper.find('.panel-heading')).to.have.length(1); 31 | }); 32 | 33 | it('should contain \'header\' with ellipsis effect', () => { 34 | const { wrapper } = setup() 35 | expect(wrapper.find('.panel-heading').find('.app-ellipsis').props().children).to.be.equal('header'); 36 | }); 37 | 38 | describe('child: icon', () => { 39 | it('should call onClick() if it is clicked', () => { 40 | const { props, wrapper } = setup() 41 | wrapper.find('.panel-heading').find('i').simulate('click'); 42 | expect(props.onClick).calledOnce; 43 | }); 44 | }); 45 | }); 46 | 47 | describe('child: count', () => { 48 | it('should render with .row', () => { 49 | const { wrapper } = setup() 50 | expect(wrapper.find('.row')).to.have.length(1); 51 | }); 52 | 53 | it('should show text "0" if count is equal to 0', () => { 54 | const props = { 55 | projectId: 'p1', 56 | header: 'header', 57 | onClick: sinon.spy(), 58 | count: 0, 59 | desc: 'description' 60 | }, 61 | wrapper = shallow(); 62 | 63 | expect(wrapper.find('.panel-count').contains( 64 | {props.count} 65 | )).to.be.true; 66 | }); 67 | 68 | it('should show count with Link if count is larger than 0', () => { 69 | const { props, wrapper } = setup() 70 | const { projectId, count } = props 71 | expect(wrapper.find('.panel-count').contains( 72 | {count} 73 | )).to.be.true; 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /tests/src/components/output/FileTypeCol.js: -------------------------------------------------------------------------------- 1 | import FileTypeCol from '../../../../src/components/output/FileTypeCol' 2 | import Col from 'react-bootstrap/lib/Col' 3 | 4 | function setup() { 5 | const props = { 6 | value: 'p', 7 | label: 'Properties', 8 | fileType: 'nj', //nested JSON is selected 9 | onChange: sinon.spy() 10 | }, 11 | wrapper = shallow(); 12 | 13 | return { 14 | props, 15 | wrapper 16 | } 17 | } 18 | 19 | describe('(component) FileTypeCol', () => { 20 | it('should render as a ', () => { 21 | const { wrapper } = setup() 22 | expect(wrapper.type()).to.eql(Col); 23 | }); 24 | 25 | describe('child: input', () => { 26 | it('should render as an ', () => { 27 | const { wrapper } = setup() 28 | expect(wrapper.props().children[0].type).to.eql('input'); 29 | }); 30 | 31 | it('should call onChange() if it is changed', () => { 32 | const { props, wrapper } = setup() 33 | wrapper.find('input[type="radio"]').last().simulate('change',{ target: { value: "p" } }); 34 | expect(props.onChange).calledOnce; 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/src/components/output/OutputPanel.js: -------------------------------------------------------------------------------- 1 | import OutputPanel from '../../../../src/components/output/OutputPanel' 2 | import CountCol from '../../../../src/components/output/CountCol' 3 | import FileTypeCol from '../../../../src/components/output/FileTypeCol' 4 | 5 | function setup() { 6 | OutputPanel.prototype.download = sinon.spy() 7 | 8 | const props = { 9 | projectCounts: { 10 | p1: 10, 11 | p2: 5 12 | } 13 | }, 14 | wrapper = shallow(); 15 | 16 | return { 17 | props, 18 | wrapper 19 | } 20 | } 21 | 22 | describe('(component) OutputPanel', () => { 23 | it('should render as a ', () => { 24 | const { wrapper } = setup() 25 | expect(wrapper.props().bsClass).to.eql('well'); 26 | }); 27 | 28 | it('should have CountCol(s)', () => { 29 | const { wrapper } = setup() 30 | expect(wrapper.find(CountCol)).to.have.length(configUtil.getProjects().length); 31 | }); 32 | 33 | it('should have 5 FileTypeCols', () => { 34 | const { wrapper } = setup() 35 | expect(wrapper.find(FileTypeCol)).to.have.length(5); 36 | }); 37 | 38 | describe('child: CountCol', () => { 39 | it('should call download() when clicked', () => { 40 | const { wrapper } = setup() 41 | wrapper.find(CountCol).get(0).props.onClick(); 42 | expect(OutputPanel.prototype.download).calledOnce; 43 | }); 44 | }); 45 | 46 | describe('child: FileTypeCol', () => { 47 | it('should call setFileType() when changed', () => { 48 | const { wrapper } = setup() 49 | wrapper.find(FileTypeCol).get(0).props.onChange(); 50 | expect(wrapper.state('fileType')).to.eql('nj'); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/src/components/vis/Tooltip.js: -------------------------------------------------------------------------------- 1 | import Tooltip from '../../../../src/components/vis/Tooltip' 2 | import timingUtil from 'keys-translations-manager-core/lib/timingUtil' 3 | 4 | function setup() { 5 | const props = { 6 | display: "none", 7 | top: 0, 8 | left: 0, 9 | ComponentActions: { 10 | hideTooltip: sinon.spy() 11 | } 12 | }, 13 | wrapper = shallow(); 14 | 15 | return { 16 | props, 17 | wrapper 18 | } 19 | } 20 | 21 | describe('(component) Tooltip', () => { 22 | it('should render as a ', () => { 23 | const { wrapper } = setup() 24 | expect(wrapper.type()).to.eql('span'); 25 | 26 | }); 27 | 28 | it('should call getTimeoutId when mouse over', () => { 29 | const { wrapper } = setup() 30 | timingUtil.getTimeoutId = sinon.spy() 31 | 32 | wrapper.find('span').get(0).props.onMouseOver(); 33 | expect(timingUtil.getTimeoutId).calledOnce; 34 | }); 35 | 36 | it('should call setTimeoutId when mouse out', () => { 37 | const { props, wrapper } = setup() 38 | timingUtil.setTimeoutId = sinon.spy() 39 | 40 | wrapper.find('span').get(0).props.onMouseOut(); 41 | expect(timingUtil.setTimeoutId).calledOnce; 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/src/components/vis/Tree.js: -------------------------------------------------------------------------------- 1 | import Tree from '../../../../src/components/vis/Tree' 2 | import Tooltip from '../../../../src/components/vis/Tooltip' 3 | import Mask from '../../../../src/components/layout/Mask' 4 | 5 | function setup() { 6 | Tree.prototype.showConfirmModal = sinon.spy(); 7 | Tree.prototype.goBack = sinon.spy(); 8 | Tree.prototype.reset = sinon.spy(); 9 | 10 | const props = { 11 | match: { 12 | params: { 13 | projectId: "p1" 14 | } 15 | }, 16 | history: {}, 17 | messages: {}, 18 | translations: [], 19 | TranslationActions: {}, 20 | ComponentActions: { 21 | showEditModal: sinon.spy(), 22 | showConfirmModal: sinon.spy(), 23 | }, 24 | CountActions: {}, 25 | VisActions: { 26 | loadTreeData: sinon.spy() 27 | }, 28 | showtooltip: false, 29 | tooltiptop: 0, 30 | tooltipleft: 0, 31 | treedata: [], 32 | reloaddata: false 33 | }, 34 | wrapper = shallow(); 35 | 36 | return { props, wrapper } 37 | } 38 | 39 | describe('(component) Tree', () => { 40 | it('should render as a
    with id "vis_tree"', () => { 41 | const { wrapper } = setup() 42 | expect(wrapper.type()).to.eql('div'); 43 | expect(wrapper.prop('id')).to.eql('vis_tree'); 44 | }); 45 | 46 | it('should have a Tooltip, a Mask, and a ButtonGroup', () => { 47 | const { wrapper } = setup(); 48 | expect(wrapper.find(Tooltip)).to.have.length(1); 49 | expect(wrapper.find(Mask)).to.have.length(1); 50 | expect(wrapper.find('ButtonGroup')).to.have.length(1); 51 | }); 52 | 53 | describe('child: Tooltip', () => { 54 | it('should contain title, content, and footer', () => { 55 | const { wrapper } = setup() 56 | expect(wrapper.find('.app-tooltip-title')).to.have.length(1); 57 | expect(wrapper.find('.app-tooltip-content')).to.have.length(1); 58 | expect(wrapper.find('.app-tooltip-footer')).to.have.length(1); 59 | }); 60 | 61 | it('should contain title, content, and footer', () => { 62 | const { wrapper } = setup() 63 | expect(wrapper.find('.app-tooltip-title')).to.have.length(1); 64 | expect(wrapper.find('.app-tooltip-content')).to.have.length(1); 65 | expect(wrapper.find('.app-tooltip-footer')).to.have.length(1); 66 | }); 67 | 68 | it('should contain description if state.desc exists', () => { 69 | const { wrapper } = setup() 70 | expect(wrapper.find('.app-tooltip-desc')).to.have.length(0); 71 | 72 | wrapper.setState({ "desc": "This is a description." }); 73 | expect(wrapper.find('.app-tooltip-desc')).to.have.length(1); 74 | }); 75 | 76 | it('should contain "trash" icon if state.data exists', () => { 77 | const { wrapper } = setup() 78 | expect(wrapper.find('i[className="fas fa-pen app-action-icon"]')).to.have.length(1); 79 | expect(wrapper.find('i[className="far fa-trash-alt app-action-icon"]')).to.have.length(0); 80 | 81 | wrapper.setState({ "data": {} }); 82 | expect(wrapper.find('i[className="far fa-trash-alt app-action-icon"]')).to.have.length(1); 83 | }); 84 | 85 | describe('child: "edit" icon', () => { 86 | it('should call showEditModal() if clicked', () => { 87 | const { props, wrapper } = setup() 88 | wrapper.find('i[className="fas fa-pen app-action-icon"]').first().simulate('click'); 89 | expect(props.ComponentActions.showEditModal).calledOnce; 90 | }); 91 | }); 92 | 93 | describe('child: "trash" icon', () => { 94 | it('should call showConfirmModal() if clicked', () => { 95 | const { wrapper } = setup(); 96 | wrapper.setState({ "data": {} }); 97 | wrapper.find('i[className="far fa-trash-alt app-action-icon"]').first().simulate('click'); 98 | expect(Tree.prototype.showConfirmModal).calledOnce; 99 | }); 100 | }); 101 | }); 102 | 103 | describe('child: "Go back" Button', () => { 104 | it('should call goBack() if clicked', () => { 105 | const { wrapper } = setup() 106 | expect(wrapper.find('Button')).to.have.length(1); 107 | 108 | wrapper.find('Button').first().simulate('click'); 109 | expect(Tree.prototype.goBack).calledOnce; 110 | }); 111 | }); 112 | 113 | describe('child: "Reset" Button', () => { 114 | it('should call reset() if clicked', () => { 115 | const { wrapper } = setup() 116 | wrapper.setState({ isTranslatedOrScaled: true }); 117 | expect(wrapper.find('Button')).to.have.length(2); 118 | 119 | wrapper.find('Button').last().simulate('click'); 120 | expect(Tree.prototype.reset).calledOnce; 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /tests/src/reducers/counts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {INIT_COUNTS} from '../../../src/constants/InitStates' 3 | import reducer from '../../../src/reducers/counts' 4 | 5 | const counts = { 6 | "p1": 24, 7 | "p2": 1 8 | }; 9 | 10 | describe('(reducer) counts', function() { 11 | it('should return the initial state', () => { 12 | expect( 13 | reducer(undefined, {}) 14 | ).to.deep.equal(INIT_COUNTS) 15 | }) 16 | 17 | it('should handle LOAD_COUNTS', () => { 18 | expect( 19 | reducer(INIT_COUNTS, { 20 | type: 'LOAD_COUNTS', 21 | counts: counts 22 | }) 23 | ).to.deep.equal(counts) 24 | }) 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /tests/src/reducers/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {INIT_ERRORS} from '../../../src/constants/InitStates' 3 | import reducer from '../../../src/reducers/errors' 4 | 5 | const errors = [{ 6 | action: "c", 7 | key: "ui.common.add", 8 | match: ["p1"], 9 | origin: null, 10 | params: { 11 | "en-US": "Add", 12 | "key": "ui.common.add", 13 | "project": ["p1", "p2"], 14 | "zh-TW": "新增" 15 | }, 16 | type: "equals" 17 | }]; 18 | 19 | describe('(reducer) errors', function() { 20 | it('should return the initial state', () => { 21 | expect( 22 | reducer(undefined, {}) 23 | ).to.deep.equal(INIT_ERRORS); 24 | }) 25 | 26 | it('should handle ALERT_ERRORS', () => { 27 | const ary = reducer(INIT_ERRORS, { 28 | type: 'ALERT_ERRORS', 29 | errors 30 | }); 31 | 32 | expect(ary) 33 | .to.be.an('array') 34 | .to.have.length.above(0) 35 | 36 | expect(ary[0]) 37 | .to.have.property('action') 38 | .that.is.a('string') 39 | .to.be.oneOf(['c', 'u']); 40 | 41 | expect(ary[0]) 42 | .to.have.property('type') 43 | .that.is.an('string') 44 | .to.be.oneOf(["emptyfield", "equals", "belongsTo", "contains"]); 45 | }) 46 | 47 | it('should handle CLEAR_ERRORS', () => { 48 | expect( 49 | reducer(errors, { 50 | type: 'LOAD_MESSAGES', 51 | errors: [] 52 | }) 53 | ) 54 | .to.be.an('array') 55 | .to.have.lengthOf(0); 56 | 57 | expect( 58 | reducer(errors, { 59 | type: 'LOAD_COUNTS', 60 | errors: [] 61 | }) 62 | ) 63 | .to.be.an('array') 64 | .to.have.lengthOf(0); 65 | 66 | expect( 67 | reducer(errors, { 68 | type: 'SHOW_EDITMODAL', 69 | errors: [] 70 | }) 71 | ) 72 | .to.be.an('array') 73 | .to.have.lengthOf(0); 74 | 75 | expect( 76 | reducer(errors, { 77 | type: 'SHOW_IMPORTMODAL', 78 | errors: [] 79 | }) 80 | ) 81 | .to.be.an('array') 82 | .to.have.lengthOf(0); 83 | 84 | expect( 85 | reducer(errors, { 86 | type: 'CLEAR_ERRORS', 87 | errors: [] 88 | }) 89 | ) 90 | .to.be.an('array') 91 | .to.have.lengthOf(0); 92 | }) 93 | }); 94 | -------------------------------------------------------------------------------- /tests/src/reducers/messages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {INIT_MESSAGES} from '../../../src/constants/InitStates' 3 | import reducer from '../../../src/reducers/messages' 4 | 5 | const lang = 'en-US' 6 | const messages = { 7 | 'ui': { 8 | 'common': { 9 | 'add': 'Add' 10 | }, 11 | 'message': { 12 | 'unread': 'You have {0} unread messages.' 13 | } 14 | } 15 | } 16 | 17 | describe('(reducer) messages', function() { 18 | it('should return the initial state', () => { 19 | expect( 20 | reducer(undefined, {}) 21 | ).to.deep.equal(INIT_MESSAGES) 22 | }) 23 | 24 | it('should handle LOAD_MESSAGES', () => { 25 | expect( 26 | reducer(INIT_MESSAGES, { 27 | type: 'LOAD_MESSAGES', 28 | lang, 29 | messages 30 | }) 31 | ).to.deep.equal({ 32 | lang: lang, 33 | messages: messages 34 | }) 35 | }) 36 | }); 37 | -------------------------------------------------------------------------------- /tests/src/reducers/socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {INIT_SOCKET} from '../../../src/constants/InitStates' 3 | import reducer from '../../../src/reducers/socket' 4 | 5 | const record = { 6 | "_id": "56e6509a7267ce4016109550", 7 | "en-US": "Add", 8 | "key": "ui.common.add", 9 | "project": ["p1", "p2"], 10 | "zh-TW": "新增" 11 | }; 12 | 13 | describe('(reducer) socket', function() { 14 | it('should return the initial state', () => { 15 | expect( 16 | reducer(undefined, {}) 17 | ).to.deep.equal(INIT_SOCKET) 18 | }) 19 | 20 | it('should handle ADD_TRANSLATION', () => { 21 | expect( 22 | reducer({ 23 | emitdatachange: false 24 | }, { 25 | type: 'ADD_TRANSLATION' 26 | }) 27 | ).to.be.an('object') 28 | .to.have.property('emitdatachange') 29 | .that.is.true 30 | }) 31 | 32 | it('should handle REMOVE_TRANSLATION', () => { 33 | expect( 34 | reducer({ 35 | emitdatachange: false 36 | }, { 37 | type: 'REMOVE_TRANSLATION' 38 | }) 39 | ).to.be.an('object') 40 | .to.have.property('emitdatachange') 41 | .that.is.true 42 | }) 43 | 44 | it('should handle UPDATE_TRANSLATION', () => { 45 | expect( 46 | reducer({ 47 | emitdatachange: false 48 | }, { 49 | type: 'UPDATE_TRANSLATION' 50 | }) 51 | ).to.be.an('object') 52 | .to.have.property('emitdatachange') 53 | .that.is.true 54 | }) 55 | 56 | it('should handle IMPORT_LOCALE', () => { 57 | expect( 58 | reducer({ 59 | emitdatachange: false 60 | }, { 61 | type: 'IMPORT_LOCALE' 62 | }) 63 | ).to.be.an('object') 64 | .to.have.property('emitdatachange') 65 | .that.is.true 66 | }) 67 | 68 | it('should handle END_DATACHANGE', () => { 69 | expect( 70 | reducer({ 71 | emitdatachange: true 72 | }, { 73 | type: 'END_DATACHANGE' 74 | }) 75 | ).to.be.an('object') 76 | .to.have.property('emitdatachange') 77 | .that.is.false 78 | }) 79 | }); 80 | -------------------------------------------------------------------------------- /tests/src/reducers/translations.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {INIT_TRANSLATIONS} from '../../../src/constants/InitStates' 3 | import reducer from '../../../src/reducers/translations' 4 | 5 | const translations = [{ 6 | "_id": "56d7037a0b70e760104ddf10", 7 | "en-US": "Edit", 8 | "key": "ui.common.edit", 9 | "project": ["p1"], 10 | "zh-TW": "編輯" 11 | }, { 12 | "_id": "56d7034f0b70e760104ddf0e", 13 | "en-US": "Add", 14 | "key": "ui.common.add", 15 | "project": ["p1"], 16 | "zh-TW": "新增" 17 | }] 18 | 19 | describe('(reducer) translations', function() { 20 | it('should return the initial state', () => { 21 | expect( 22 | reducer(undefined, {}) 23 | ).to.deep.equal(INIT_TRANSLATIONS) 24 | }) 25 | 26 | it('should handle ADD_TRANSLATION', () => { 27 | expect( 28 | reducer(translations, { 29 | type: 'ADD_TRANSLATION', 30 | data: { 31 | "_id": "56d7038b0b70e760104ddf11", 32 | "en-US": "Delete", 33 | "key": "ui.common.delete", 34 | "project": ["p1"], 35 | "zh-TW": "刪除" 36 | } 37 | }) 38 | ).to.deep.equal([{ 39 | "_id": "56d7038b0b70e760104ddf11", 40 | "en-US": "Delete", 41 | "key": "ui.common.delete", 42 | "project": ["p1"], 43 | "zh-TW": "刪除" 44 | }, { 45 | "_id": "56d7037a0b70e760104ddf10", 46 | "en-US": "Edit", 47 | "key": "ui.common.edit", 48 | "project": ["p1"], 49 | "zh-TW": "編輯" 50 | }, { 51 | "_id": "56d7034f0b70e760104ddf0e", 52 | "en-US": "Add", 53 | "key": "ui.common.add", 54 | "project": ["p1"], 55 | "zh-TW": "新增" 56 | }]) 57 | }) 58 | 59 | it('should handle LOAD_TRANSLATIONS', () => { 60 | expect( 61 | reducer(INIT_TRANSLATIONS, { 62 | type: 'LOAD_TRANSLATIONS', 63 | data: translations 64 | }) 65 | ).to.deep.equal(translations) 66 | }) 67 | 68 | it('should handle IMPORT_LOCALE', () => { 69 | expect( 70 | reducer(INIT_TRANSLATIONS, { 71 | type: 'IMPORT_LOCALE', 72 | data: translations 73 | }) 74 | ).to.deep.equal(translations) 75 | }) 76 | 77 | it('should handle MERGE_TRANSLATIONS', () => { 78 | expect( 79 | reducer(INIT_TRANSLATIONS, { 80 | type: 'MERGE_TRANSLATIONS', 81 | data: translations 82 | }) 83 | ).to.deep.equal(translations) 84 | }) 85 | 86 | it('should handle REMOVE_TRANSLATION', () => { 87 | expect( 88 | reducer(translations, { 89 | type: 'REMOVE_TRANSLATION', 90 | id: "56d7037a0b70e760104ddf10" 91 | }) 92 | ).to.deep.equal([{ 93 | "_id": "56d7034f0b70e760104ddf0e", 94 | "en-US": "Add", 95 | "key": "ui.common.add", 96 | "project": ["p1"], 97 | "zh-TW": "新增" 98 | }]) 99 | }) 100 | 101 | it('should handle UPDATE_TRANSLATION', () => { 102 | expect( 103 | reducer(translations, { 104 | type: 'UPDATE_TRANSLATION', 105 | data: { 106 | "_id": "56d7034f0b70e760104ddf0e", 107 | "en-US": "Add", 108 | "key": "ui.common.add", 109 | "project": ["p1", "p2"], 110 | "zh-TW": "增加" 111 | } 112 | }) 113 | ).to.deep.equal([{ 114 | "_id": "56d7037a0b70e760104ddf10", 115 | "en-US": "Edit", 116 | "key": "ui.common.edit", 117 | "project": ["p1"], 118 | "zh-TW": "編輯" 119 | }, { 120 | "_id": "56d7034f0b70e760104ddf0e", 121 | "en-US": "Add", 122 | "key": "ui.common.add", 123 | "project": ["p1", "p2"], 124 | "zh-TW": "增加" 125 | }]) 126 | }) 127 | }); 128 | -------------------------------------------------------------------------------- /tests/src/reducers/vis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {INIT_VIS} from '../../../src/constants/InitStates' 3 | import reducer from '../../../src/reducers/vis' 4 | 5 | const data = [{ 6 | "name": "ui", 7 | "children": [{ 8 | "name": "common", 9 | "children": [{ 10 | "name": "delete", 11 | "translations": { 12 | "description": "", 13 | "key": "ui.common.delete", 14 | "en-US": "Delete", 15 | "zh-TW": "刪除", 16 | "_id": "577a868da4d9538f0f7e4ef6", 17 | "__v": 0, 18 | "project": ["p1", "p2"] 19 | } 20 | }, { 21 | "name": "add", 22 | "translations": { 23 | "description": "", 24 | "key": "ui.common.add", 25 | "en-US": "Add", 26 | "zh-TW": "新增", 27 | "_id": "577a8684a4d9538f0f7e4ef5", 28 | "__v": 0, 29 | "project": ["p1", "p2"] 30 | } 31 | }] 32 | }] 33 | }]; 34 | 35 | describe('(reducer) vis', function() { 36 | it('should return the initial state', () => { 37 | expect( 38 | reducer(undefined, {}) 39 | ).to.deep.equal(INIT_VIS) 40 | }) 41 | 42 | it('should handle LOAD_TREE_DATA', () => { 43 | expect( 44 | reducer({ 45 | treedata: null 46 | }, { 47 | type: "LOAD_TREE_DATA", 48 | data: data 49 | }) 50 | ).to.deep.equal({ 51 | treedata: data 52 | }) 53 | }) 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /tests/testHelper.js: -------------------------------------------------------------------------------- 1 | import ES6Promise from 'es6-promise' 2 | ES6Promise.polyfill(); 3 | import 'isomorphic-fetch' 4 | import jsdom from 'jsdom' 5 | import React from 'react' 6 | import chai from 'chai' 7 | import sinon from 'sinon' 8 | import sinonChai from 'sinon-chai' 9 | import configureStore from 'redux-mock-store' 10 | import nock from 'nock' 11 | import thunk from 'redux-thunk' 12 | import config from '../ktm.config' 13 | import configUtil from '../src/configUtil' 14 | import Enzyme from 'enzyme' 15 | import Adapter from 'enzyme-adapter-react-16' 16 | 17 | Enzyme.configure({ adapter: new Adapter() }) 18 | 19 | const { JSDOM } = jsdom 20 | const dom = new JSDOM('') 21 | // const win = doc.defaultView 22 | const expect = chai.use(sinonChai).expect 23 | const middlewares = [thunk]; 24 | const mockStore = configureStore(middlewares) 25 | 26 | global.document = dom.window.document 27 | global.window = dom.window 28 | global.navigator = window.navigator 29 | global.React = React 30 | global.sinon = sinon 31 | global.expect = expect 32 | global.configureStore = configureStore 33 | global.nock = nock 34 | global.config = config 35 | global.configUtil = configUtil 36 | global.mockStore = mockStore 37 | global.shallow = Enzyme.shallow 38 | global.mount = Enzyme.mount 39 | global.render = Enzyme.render 40 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Keys-Translations Manager 5 | 6 | 7 | 8 | 9 | 10 | 11 | <%- css %> 12 | 13 | 14 | 15 | 19 | 20 | 21 |
    22 | <%- markup %> 23 |
    24 | <%- initialState %> 25 | 26 | 27 | 28 | 29 | <%- vendor %> 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var dir = { 4 | src: path.join(__dirname, 'src'), 5 | dist: path.join(__dirname, 'public', 'js') 6 | }; 7 | 8 | module.exports = { 9 | mode: 'development', 10 | entry: [ 11 | 'eventsource-polyfill', // necessary for hot reloading with IE 12 | 'webpack-hot-middleware/client', 13 | path.join(dir.src, 'client', 'index') 14 | ], 15 | output: { 16 | path: dir.dist, 17 | filename: 'bundle.js', 18 | publicPath: '/public/js/' 19 | }, 20 | plugins: [ 21 | new webpack.HotModuleReplacementPlugin(), 22 | new webpack.NoEmitOnErrorsPlugin(), 23 | new webpack.DefinePlugin({ 24 | '__DEV__': true 25 | }) 26 | ], 27 | devtool: 'cheap-module-eval-source-map', 28 | resolve: { 29 | extensions: ['.js', '.jsx'], 30 | alias: { 31 | 'react-dom': '@hot-loader/react-dom' 32 | }, 33 | }, 34 | module: { 35 | rules: [{ 36 | test: /\.jsx?$/, 37 | enforce: 'pre', 38 | exclude: path.resolve(__dirname, 'node_modules'), 39 | use: ['eslint-loader'] 40 | }, { 41 | test: /\.jsx?$/, 42 | use: ['babel-loader'], 43 | include: dir.src 44 | }, { 45 | test: /\.(css|less)$/, 46 | use: [ 47 | "style-loader", 48 | "css-loader", 49 | "less-loader" 50 | ] 51 | }] 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var CleanWebpackPlugin = require('clean-webpack-plugin').CleanWebpackPlugin; 4 | var WebpackStrip = require('webpack-strip'); 5 | var dir = { 6 | src: path.join(__dirname, 'src'), 7 | dist: path.join(__dirname, 'public', 'js') 8 | }; 9 | 10 | var config = { 11 | mode: 'production', 12 | entry: { 13 | bundle: path.join(dir.src, 'client', 'index') 14 | }, 15 | output: { 16 | path: dir.dist, 17 | filename: "[name].js", 18 | publicPath: '/public/js/' 19 | }, 20 | optimization: { 21 | splitChunks: { 22 | cacheGroups: { 23 | vendor: { 24 | test: /[\\/]node_modules[\\/](react|react-dom|redux|redux-thunk|react-redux|react-router|react-router-dom|socket.io-client)[\\/]/, 25 | name: 'vendor', 26 | chunks: 'all', 27 | } 28 | } 29 | } 30 | }, 31 | plugins: [ 32 | new CleanWebpackPlugin({ 33 | cleanOnceBeforeBuildPatterns: ['./public/js'], 34 | }), 35 | new webpack.DefinePlugin({ 36 | '__DEV__': false 37 | }) 38 | ], 39 | devtool: 'source-map', 40 | resolve: { 41 | extensions: ['.js', '.jsx'], 42 | }, 43 | module: { 44 | rules: [{ 45 | test: /\.jsx?$/, 46 | exclude: path.resolve(__dirname, 'node_modules'), 47 | use: ['babel-loader', WebpackStrip.loader('console.log', 'console.warn')] 48 | }, { 49 | test: /\.(css|less)$/, 50 | use: [ 51 | "style-loader", 52 | "css-loader", 53 | "less-loader" 54 | ] 55 | }/*, { 56 | test: /\.(png|jpg|gif|ttf|woff|woff2)$/, 57 | loader: 'url?limit=25000' 58 | }*/] 59 | } 60 | }; 61 | 62 | module.exports = config; 63 | --------------------------------------------------------------------------------