├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── packages ├── clawler │ ├── .env.development │ ├── .env.production │ ├── LICENSE │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── dev │ │ │ ├── backend │ │ │ │ ├── index.ts │ │ │ │ ├── phrase.ts │ │ │ │ ├── tmp │ │ │ │ │ └── .gitkeep │ │ │ │ └── utils.ts │ │ │ └── frontend │ │ │ │ ├── App.vue │ │ │ │ ├── components │ │ │ │ ├── Hello.vue │ │ │ │ ├── Meta.vue │ │ │ │ ├── Off.vue │ │ │ │ └── On.vue │ │ │ │ ├── i18n.ts │ │ │ │ ├── locales │ │ │ │ ├── en-US.json │ │ │ │ └── ja-JP.json │ │ │ │ ├── main.ts │ │ │ │ ├── mixin.ts │ │ │ │ ├── pages │ │ │ │ ├── About.vue │ │ │ │ └── Home.vue │ │ │ │ └── router.ts │ │ ├── helper.ts │ │ ├── hook.ts │ │ ├── main.ts │ │ ├── types.ts │ │ └── worker.ts │ ├── tsconfig.json │ └── vite.config.ts ├── devtools │ ├── LICENSE │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── components │ │ │ └── HelloWorld.vue │ │ ├── main.ts │ │ ├── types.ts │ │ └── worker.ts │ ├── tsconfig.json │ └── vite.config.ts ├── shared │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── babel.ts │ │ ├── crypt.ts │ │ ├── index.ts │ │ ├── mixin.ts │ │ └── sfc.ts │ ├── test │ │ ├── babel.test.ts │ │ └── sfc.test.ts │ └── tsconfig.json ├── shell-dev │ ├── LICENSE │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── assets │ │ │ └── logo.png │ │ ├── components │ │ │ └── HelloWorld.vue │ │ └── main.ts │ ├── tsconfig.json │ └── vite.config.ts └── vite-plugin-intlify-devtools │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ └── index.ts │ ├── test │ └── .gitkeep │ └── tsconfig.json ├── renovate.json ├── scripts ├── checkYarn.js ├── fixpack.ts ├── generateMetaSecret.ts └── utils.ts ├── ship.config.js ├── tsconfig.base.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | lib 3 | ./jest.config.js 4 | ./test/fixtures 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | root: true, 5 | globals: { 6 | page: true, 7 | browser: true, 8 | context: true 9 | }, 10 | env: { 11 | node: true, 12 | jest: true 13 | }, 14 | extends: [ 15 | 'plugin:vue-libs/recommended', 16 | 'plugin:vue/vue3-recommended', 17 | 'plugin:@typescript-eslint/recommended', 18 | 'plugin:@typescript-eslint/eslint-recommended', 19 | 'plugin:prettier/recommended', 20 | 'prettier' 21 | ], 22 | plugins: ['@typescript-eslint'], 23 | parser: 'vue-eslint-parser', 24 | parserOptions: { 25 | parser: '@typescript-eslint/parser', 26 | sourceType: 'module' 27 | }, 28 | rules: { 29 | 'object-curly-spacing': 'off', 30 | 'vue/valid-template-root': 'off', 31 | 'vue/no-multiple-template-root': 'off', 32 | '@typescript-eslint/ban-ts-comment': 'off' 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.json linguist-language=JSON-with-Comments -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: kazupon 4 | patreon: # 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: # Replace with a single custom sponsorship URL 9 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release') 12 | runs-on: Ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: actions/setup-node@v1 16 | with: 17 | registry-url: "https://registry.npmjs.org" 18 | - run: git switch master 19 | - run: | 20 | if [ -f "yarn.lock" ]; then 21 | yarn install 22 | else 23 | npm install 24 | fi 25 | - run: npm run release:trigger 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 29 | SLACK_INCOMING_HOOK: ${{ secrets.SLACK_INCOMING_HOOK }} 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches-ignore: 5 | - gh-pages 6 | pull_request: 7 | env: 8 | CI: true 9 | 10 | jobs: 11 | test: 12 | name: "Test on Node.js ${{ matrix.node }} OS: ${{matrix.os}}" 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, windows-latest, macos-latest] 17 | node: [12, 14, 15] 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | - name: Setup Node.js ${{ matrix.node }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node }} 25 | - name: Install 26 | run: yarn install 27 | - name: Test 28 | run: yarn test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | .local 4 | coverage 5 | node_modules 6 | tmp/*.png 7 | dist 8 | dist-ssr 9 | lib 10 | *.tsbuildinfo 11 | *.local 12 | *.log 13 | *.swp 14 | *.traineddata 15 | *~ 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.log 3 | *.swp 4 | coverage 5 | __snapshots__ 6 | test 7 | .vscode 8 | examples -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib 2 | coverage 3 | tsconfig.json 4 | packages/**/tsconfig.json 5 | dist 6 | packages/**/dist 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | printWidth: 80 4 | trailingComma: "none" 5 | endOfLine: "auto" 6 | arrowParens: "avoid" 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intlify/devtools/2b3d932564dcd7c94af3bd02a32ba79a736a7e4c/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 kazuya kawaguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # devtools 2 | 3 | :gear: i18n devtools for debugging Intlify applications 4 | 5 | WIP 6 | 7 | ## :copyright: License 8 | 9 | [MIT](http://opensource.org/licenses/MIT) 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/dr/y00s2v7d6xs144hbxgl5z2cw0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | collectCoverageFrom: ['packages/*/src/**/*.ts'], 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: 'coverage', 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | coveragePathIgnorePatterns: ['/node_modules/', '/test/*.*'], 31 | 32 | // A list of reporter names that Jest uses when writing coverage reports 33 | // coverageReporters: [ 34 | // "json", 35 | // "text", 36 | // "lcov", 37 | // "clover" 38 | // ], 39 | 40 | // An object that configures minimum threshold enforcement for coverage results 41 | // coverageThreshold: null, 42 | 43 | // A path to a custom dependency extractor 44 | // dependencyExtractor: null, 45 | 46 | // Make calling deprecated APIs throw helpful error messages 47 | // errorOnDeprecated: false, 48 | 49 | // Force coverage collection from ignored files using an array of glob patterns 50 | // forceCoverageMatch: [], 51 | 52 | // A path to a module which exports an async function that is triggered once before all test suites 53 | // globalSetup: null, 54 | 55 | // A path to a module which exports an async function that is triggered once after all test suites 56 | // globalTeardown: null, 57 | 58 | // A set of global variables that need to be available in all test environments 59 | globals: { 60 | 'ts-jest': { 61 | diagnostics: false 62 | } 63 | }, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "json", 77 | // "jsx", 78 | // "ts", 79 | // "tsx", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | preset: 'ts-jest', 97 | 98 | // Run tests from one or more projects 99 | // projects: null, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state between every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: null, 112 | 113 | // Automatically restore mock state between every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | rootDir: __dirname, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 134 | // snapshotSerializers: [], 135 | 136 | // The test environment that will be used for testing 137 | testEnvironment: 'node', 138 | 139 | // Options that will be passed to the testEnvironment 140 | // testEnvironmentOptions: {}, 141 | 142 | // Adds a location field to test results 143 | // testLocationInResults: false, 144 | 145 | // The glob patterns Jest uses to detect test files 146 | testMatch: ['/packages/**/test/**/*(*.)@(spec|test).[tj]s?(x)'], 147 | 148 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 149 | // testPathIgnorePatterns: [ 150 | // "/node_modules/" 151 | // ], 152 | 153 | // The regexp pattern or array of patterns that Jest uses to detect test files 154 | // testRegex: [], 155 | 156 | // This option allows the use of a custom results processor 157 | // testResultsProcessor: null, 158 | 159 | // This option allows use of a custom test runner 160 | // testRunner: "jasmine2", 161 | 162 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 163 | // testURL: "http://localhost", 164 | 165 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 166 | // timers: "real", 167 | 168 | // A map from regular expressions to paths to transformers 169 | // transform: null, 170 | 171 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 172 | // transformIgnorePatterns: [ 173 | // "/node_modules/" 174 | // ], 175 | 176 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 177 | // unmockedModulePathPatterns: undefined, 178 | 179 | // Indicates whether each individual test should be reported during the run 180 | // verbose: null, 181 | 182 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 183 | watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/.git/'], 184 | 185 | // Whether to use watchman for file crawling 186 | // watchman: true, 187 | 188 | watchPlugins: [ 189 | 'jest-watch-typeahead/filename', 190 | 'jest-watch-typeahead/testname' 191 | ] 192 | } 193 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devtools", 3 | "description": "i18n devtools for debugging Intlify applications", 4 | "version": "0.0.0", 5 | "author": { 6 | "name": "kazuya kawaguchi", 7 | "email": "kawakazu80@gmail.com" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/inlitify/devtools/issues" 11 | }, 12 | "changelog": { 13 | "labels": { 14 | "Type: Feature": ":star: Features", 15 | "Type: Bug": ":bug: Bug Fixes", 16 | "Type: Security": ":lock: Security Fixes", 17 | "Type: Performance": ":chart_with_upwards_trend: Performance Fixes", 18 | "Type: Improvement": ":zap: Improvement Features", 19 | "Type: Breaking": ":boom: Breaking Change", 20 | "Type: Deprecated": ":warning: Deprecated Features", 21 | "Type: I18n": ":globe_with_meridians: Internationalization", 22 | "Type: A11y": ":wheelchair: Accessibility", 23 | "Type: Documentation": ":pencil: Documentation" 24 | } 25 | }, 26 | "devDependencies": { 27 | "@intlify/vite-plugin-vue-i18n": "^2.1.2", 28 | "@types/jest": "^26.0.20", 29 | "@types/node": "^14.14.37", 30 | "@typescript-eslint/eslint-plugin": "^4.21.0", 31 | "@typescript-eslint/parser": "^4.21.0", 32 | "@vitejs/plugin-vue": "^1.2.1", 33 | "@vue/compiler-sfc": "^3.0.11", 34 | "@vuedx/typecheck": "^0.6.0", 35 | "@vuedx/typescript-plugin-vue": "^0.6.0", 36 | "chalk": "^4.1.0", 37 | "concurrently": "^6.0.0", 38 | "cross-env": "^7.0.3", 39 | "esbuild-register": "^2.3.0", 40 | "eslint": "^7.23.0", 41 | "eslint-config-prettier": "^8.1.0", 42 | "eslint-plugin-prettier": "^3.3.0", 43 | "eslint-plugin-vue": "^7.7.0", 44 | "eslint-plugin-vue-libs": "^4.0.0", 45 | "fixpack": "^4.0.0", 46 | "jest": "^26.6.0", 47 | "jest-watch-typeahead": "^0.6.1", 48 | "lerna-changelog": "^1.0.1", 49 | "npm-run-all": "^4.1.5", 50 | "prettier": "^2.2.1", 51 | "shipjs": "^0.23.0", 52 | "ts-jest": "^26.5.0", 53 | "typescript": "^4.2.3", 54 | "typescript-eslint-language-service": "^4.1.3", 55 | "vite": "^2.1.5" 56 | }, 57 | "engines": { 58 | "node": ">= 12" 59 | }, 60 | "keywords": [ 61 | "dev-server", 62 | "devtools", 63 | "i18n", 64 | "intlify", 65 | "vite" 66 | ], 67 | "license": "MIT", 68 | "private": true, 69 | "repository": { 70 | "type": "git", 71 | "url": "git+https://github.com/intlify/devtools.git" 72 | }, 73 | "scripts": { 74 | "build": "yarn build:shared && yarn build:vite-plugin && yarn build:devtools && yarn build:clawler && yarn build:shell-dev", 75 | "build:clawler": "cd packages/clawler && yarn build", 76 | "build:devtools": "cd packages/devtools && yarn build", 77 | "build:shared": "cd packages/shared && yarn build", 78 | "build:shell-dev": "cd packages/shell-dev && yarn build", 79 | "build:vite-plugin": "cd packages/vite-plugin-intlify-devtools && yarn build", 80 | "clean:cache": "yarn clean:cache:jest", 81 | "clean:cache:jest": "jest --clearCache", 82 | "clean:deps": "npm-run-all --parallel clean:deps:*", 83 | "clean:deps:clawler": "cd packages/clawler && rm -rf node_modules", 84 | "clean:deps:devtools": "cd packages/devtools && rm -rf node_modules", 85 | "clean:deps:shared": "cd packages/shared && rm -rf node_modules && rm -rf tsconfig.tsbuildinfo", 86 | "clean:deps:shell-dev": "cd packages/shell-dev && rm -rf node_modules", 87 | "clean:deps:vite-plugin": "cd packages/vite-plugin-intlify-devtools && rm -rf node_modules && rm -rf tsconfig.tsbuildinfo", 88 | "dev": "concurrently -c 'blue.bold,magenta.bold' 'npm:serve:devtools' 'npm:dev:shell-dev'", 89 | "dev:clawler": "cd packages/clawler && yarn dev", 90 | "dev:devtools": "cd packages/devtools && yarn dev", 91 | "dev:shell-dev": "cd packages/shell-dev && yarn dev", 92 | "fix": "npm-run-all --parallel lint:fix format:fix", 93 | "format:fix": "npm-run-all --parallel \"format:prettier --write\" format:package", 94 | "format:package": "node -r esbuild-register ./scripts/fixpack.ts", 95 | "format:prettier": "prettier --config .prettierrc --ignore-path .prettierignore '**/*.{js,json,html}'", 96 | "lint": "npm-run-all --parallel lint:codes", 97 | "lint:codes": "eslint ./packages --ext .js,.ts,.vue", 98 | "lint:fix": "npm-run-all --parallel \"lint:codes --fix\"", 99 | "preinstall": "node ./scripts/checkYarn.js", 100 | "release:prepare": "shipjs prepare", 101 | "release:trigger": "shipjs trigger", 102 | "serve:clawler": "cd packages/clawler && yarn serve --port 5002", 103 | "serve:devtools": "cd packages/devtools && yarn serve --port 5001", 104 | "setup:secret": "node -r esbuild-register ./scripts/generateMetaSecret.ts", 105 | "test": "npm-run-all test:unit", 106 | "test:cover": "yarn test:unit --coverage", 107 | "test:unit": "yarn clean:cache:jest && jest --env node", 108 | "watch": "concurrently -c 'blue.bold,magenta.bold' 'npm:watch:shared' 'npm:watch:vite-plugin'", 109 | "watch:shared": "cd packages/shared && yarn watch", 110 | "watch:vite-plugin": "cd packages/vite-plugin-intlify-devtools && yarn watch" 111 | }, 112 | "workspaces": [ 113 | "packages/*" 114 | ] 115 | } 116 | -------------------------------------------------------------------------------- /packages/clawler/.env.development: -------------------------------------------------------------------------------- 1 | VITE_BASE_ENDPOINT = http://localhost:3200/bend -------------------------------------------------------------------------------- /packages/clawler/.env.production: -------------------------------------------------------------------------------- 1 | VITE_BASE_ENDPOINT = https://devtools.intlify.dev -------------------------------------------------------------------------------- /packages/clawler/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 kazuya kawaguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/clawler/README.md: -------------------------------------------------------------------------------- 1 | # @intlify/clawler 2 | 3 | ## :copyright: License 4 | 5 | [MIT](http://opensource.org/licenses/MIT) 6 | -------------------------------------------------------------------------------- /packages/clawler/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Clawler App 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/clawler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@intlify/clawler", 3 | "version": "0.0.0", 4 | "bugs": { 5 | "url": "https://github.com/intlify/devtools/issues" 6 | }, 7 | "dependencies": { 8 | "@intlify-devtools/shared": "0.0.0", 9 | "@intlify/worker-dom": "^1.3.1", 10 | "@intlify/shared": "^9.1.3", 11 | "@intlify/devtools-if": "^9.1.3", 12 | "phrase-js": "^1.0.10", 13 | "form-data": "^3.0.0", 14 | "node-fetch": "^2.6.0", 15 | "html2canvas": "^1.0.0-rc.7" 16 | }, 17 | "devDependencies": { 18 | "@intlify/vite-plugin-vue-i18n": "^2.1.2", 19 | "@types/express": "^4.17.11", 20 | "@types/node-fetch": "^2.5.10", 21 | "chalk": "^4.1.0", 22 | "cross-env": "^7.0.3", 23 | "dotenv": "^8.2.0", 24 | "express": "^4.17.1", 25 | "puppeteer": "^8.0.0", 26 | "tesseract.js": "^2.1.4", 27 | "vue": "^3.0.11", 28 | "vue-i18n": "^9.1.4", 29 | "vue-router": "^4.0.5" 30 | }, 31 | "peerDependencies": { 32 | "vue-i18n": "^9.1.3" 33 | }, 34 | "engines": { 35 | "node": ">= 12" 36 | }, 37 | "files": [ 38 | "dist/clawler.es.js", 39 | "dist/worker.js" 40 | ], 41 | "homepage": "https://github.com/intlify/devtools/tree/master/packages/clawler#readme", 42 | "module": "dist/clawler.es.js", 43 | "private": true, 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/intlify/devtools.git", 47 | "directory": "packages/clawler" 48 | }, 49 | "scripts": { 50 | "build": "vite build", 51 | "dev": "cross-env PORT=4000 concurrently -c 'blue.bold,magenta.bold' 'npm:dev:backend' 'npm:dev:frontend'", 52 | "dev:backend": "node -r esbuild-register ./src/dev/backend/index.ts", 53 | "dev:frontend": "vite", 54 | "serve": "vite preview" 55 | }, 56 | "sideEffects": false 57 | } 58 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/backend/index.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import path from 'path' 3 | import express, { json, urlencoded } from 'express' 4 | import chalk from 'chalk' 5 | import { config as dotEnvConfig } from 'dotenv' 6 | import { 7 | generateSecret, 8 | decrypt, 9 | getResourceKeys as getResourceI18nKeys 10 | } from '@intlify-devtools/shared' 11 | import { screenshot, recognize } from './utils' 12 | import { 13 | getPhraseInfo, 14 | getKeys, 15 | uploadResources, 16 | uploadResource, 17 | uploadScreenshot 18 | } from './phrase' 19 | 20 | declare global { 21 | namespace Express { 22 | interface Request { 23 | phraseInfo?: PhraseInfo 24 | } 25 | } 26 | } 27 | 28 | import type { IntlifyDevToolsHookPayloads } from '@intlify/devtools-if' 29 | import type { Page, Line, Word } from 'tesseract.js' 30 | import type { PhraseInfo } from './phrase' 31 | 32 | const LOCAL_ENV = dotEnvConfig({ path: './.env.local' }).parsed || {} 33 | // @ts-ignore 34 | const SECRET = 35 | LOCAL_ENV.INTLIFY_META_SECRET || 36 | process.env.INTLIFY_META_SECRET || 37 | // @ts-ignore 38 | generateSecret() 39 | const PORT = process.env.PORT || 4000 40 | 41 | const STORE = new Map() 42 | 43 | export type AnalisysLocalization = { 44 | url: string 45 | components: Map< 46 | string, 47 | { 48 | path: string 49 | devtools: Set 50 | } 51 | > 52 | screenshot?: string 53 | recoganize?: Page 54 | detecting?: Map< 55 | string, 56 | { 57 | lineOrWord: Line | Word 58 | devtool: IntlifyDevToolsHookPayloads['function:translate'] 59 | } 60 | > 61 | notyet?: Map 62 | } 63 | 64 | const STORE2 = new Map() 65 | 66 | const app = express() 67 | app.use(json({ limit: '200mb' })) // TODO: change to no limit option 68 | app.use(urlencoded({ limit: '200mb', extended: true })) // TODO: change to no limit option 69 | app.use((req, res, next) => { 70 | // for CORS, TODO: change to another ways 71 | res.header('Access-Control-Allow-Origin', '*') 72 | res.header('Access-Control-Allow-Methods', '*') 73 | res.header('Access-Control-Allow-Headers', '*') 74 | next() 75 | }) 76 | app.use(async (req, res, next) => { 77 | if (req.phraseInfo == null) { 78 | req.phraseInfo = await getPhraseInfo() 79 | // const keys = await getKeys(req.phraseInfo!) 80 | // console.log('keys', keys) 81 | // const uploads = await uploadResources(req.phraseInfo!) 82 | // console.log('uploads', uploads) 83 | } 84 | next() 85 | }) 86 | 87 | function setComponentPath(paths: string[], components: { paths: Set }) { 88 | paths.forEach(p => { 89 | const [iv, encrypedData] = p.split('$') 90 | const componentPath = decrypt(SECRET, iv, encrypedData) 91 | // console.log('path', componentPath) 92 | components.paths.add(componentPath) 93 | }) 94 | } 95 | 96 | async function getResourceKeys(paths: string[]) { 97 | const ret: Record = {} 98 | for (const p of paths) { 99 | const file = await fs.readFile(p, 'utf-8') 100 | const keys = getResourceI18nKeys(file) 101 | if (keys.length) { 102 | ret[p] = keys 103 | } 104 | } 105 | console.log('getResourceKeys', ret) 106 | return ret 107 | } 108 | 109 | function analysysDevTools( 110 | l10n: AnalisysLocalization, 111 | devtools: IntlifyDevToolsHookPayloads['function:translate'][] 112 | ) { 113 | for (const devtool of devtools) { 114 | if (devtool.meta?.__INTLIFY_META__) { 115 | const crypedPath = devtool.meta?.__INTLIFY_META__ as string 116 | const [iv, encrypedData] = crypedPath.split('$') 117 | const componentPath = decrypt(SECRET, iv, encrypedData) 118 | console.log('path', componentPath) 119 | if (l10n.components.has(crypedPath)) { 120 | l10n.components.get(crypedPath)?.devtools.add(devtool) 121 | } else { 122 | const comp = { 123 | path: componentPath, 124 | devtools: new Set() 125 | } 126 | comp.devtools.add(devtool) 127 | l10n.components.set(crypedPath, comp) 128 | } 129 | } 130 | } 131 | console.log('l10n', l10n.components) 132 | return l10n 133 | } 134 | 135 | const normalizeOCRText = (text: string): string => 136 | text.trim().replace(/\r?\n/g, '') 137 | 138 | function detectWithDevTools(l10n: AnalisysLocalization, data: Page) { 139 | l10n.detecting = new Map() 140 | l10n.notyet = new Map() 141 | 142 | // TODO: need to refactoring 143 | const notDetect = (l: Line | Word) => { 144 | return ![...l10n.detecting!.values()].some(value => value.lineOrWord === l) 145 | } 146 | 147 | for (const word of data.words) { 148 | const lineText = normalizeOCRText(word.line.text) 149 | let lineFound = false 150 | for (const [key, { devtools }] of l10n.components) { 151 | for (const devtool of devtools.values()) { 152 | if (devtool.message === lineText) { 153 | lineFound = true 154 | if (!l10n.detecting!.has(devtool.message)) { 155 | l10n.detecting!.set(devtool.message, { 156 | lineOrWord: word.line, 157 | devtool 158 | }) 159 | } else { 160 | console.log('already register', devtool.message) 161 | } 162 | } 163 | } 164 | } 165 | if (!lineFound) { 166 | // TODO: refactoring 167 | for (const [key, { devtools }] of l10n.components) { 168 | for (const devtool of devtools.values()) { 169 | if (devtool.message === word.text) { 170 | lineFound = true 171 | if (!l10n.detecting!.has(devtool.message)) { 172 | l10n.detecting!.set(devtool.message, { 173 | lineOrWord: word, 174 | devtool 175 | }) 176 | } else { 177 | console.log('already register', devtool.message) 178 | } 179 | } 180 | } 181 | } 182 | } 183 | 184 | if (!lineFound) { 185 | l10n.notyet.set( 186 | lineText !== word.text ? lineText : word.text, 187 | lineText !== word.text ? word.line : word 188 | ) 189 | // l10n.notyet.set(lineText, word.line) 190 | } 191 | } 192 | } 193 | 194 | app.get('/upload', async (req, res) => { 195 | console.log('request upload ...') 196 | console.log('req.query', req.query) 197 | const { sh, url } = req.query 198 | 199 | if (sh) { 200 | if (url) { 201 | const filepath = path.resolve(__dirname, './tmp/screenshot.png') 202 | await screenshot(url, filepath) 203 | // @ts-ignore 204 | const l10n: AnalisysLocalization = STORE2.has(url) 205 | ? STORE2.get(url) 206 | : { url, components: new Map(), detecting: new Map() } 207 | const buffer = await fs.readFile(filepath, 'base64') 208 | const data = `data:image/png;base64,${buffer}` 209 | const result = l10n.recoganize 210 | ? l10n.recoganize 211 | : (await recognize(data)).data 212 | l10n.recoganize = result 213 | l10n.screenshot = data 214 | if (!l10n.detecting) { 215 | detectWithDevTools(l10n, result) 216 | } 217 | await uploadScreenshot(req.phraseInfo!, l10n, filepath) 218 | // console.log('detect', l10n.detecting) 219 | } 220 | console.log('upload screenshot status') 221 | res.status(200).json({ 222 | stat: 'ok' 223 | }) 224 | } else { 225 | const fileTarget = path.resolve(__dirname, '../frontend/locales/en-US.json') 226 | console.log('filetarget', fileTarget) 227 | const ret = await uploadResource(req.phraseInfo!, fileTarget) 228 | console.log('upload resource status', ret) 229 | res.status(200).json({ 230 | stat: 'ok' 231 | }) 232 | } 233 | }) 234 | 235 | app.get('/', async (req, res) => { 236 | console.log('req.query', req.query) 237 | const { url, locale } = req.query 238 | 239 | const components = STORE.has(url) ? STORE.get(url) : { paths: new Set() } 240 | if (!STORE.has(url)) { 241 | STORE.set(url, components) 242 | } 243 | 244 | // @ts-ignore 245 | const l10n: AnalisysLocalization = STORE2.has(url) 246 | ? STORE2.get(url) 247 | : { url, components: new Map(), detecting: new Map() } 248 | console.log('/ get', l10n) 249 | 250 | const keys = await getResourceKeys([...components.paths]) 251 | 252 | // @ts-ignore 253 | const data = l10n.screenshot ? l10n.screenshot : await screenshot(url) 254 | if (data) { 255 | l10n.screenshot = data 256 | const result = l10n.recoganize 257 | ? l10n.recoganize 258 | : (await recognize(data)).data 259 | // console.log(d) 260 | l10n.recoganize = result 261 | if (!l10n.detecting) { 262 | detectWithDevTools(l10n, result) 263 | } 264 | console.log('detect', l10n.detecting) 265 | // serializeDetecting(l10n) 266 | } 267 | 268 | res.status(200).json({ 269 | url, 270 | keys, 271 | paths: [...components.paths], 272 | screenshot: components.screenshot, 273 | recognize: components.recognize, 274 | detecting: [...l10n.detecting!.values()], 275 | notyet: [...l10n.notyet!.values()] 276 | }) 277 | }) 278 | 279 | app.post('/', async (req, res) => { 280 | const { 281 | url, 282 | meta, 283 | added, 284 | removed, 285 | locale, 286 | /* screenshot, */ timestamp, 287 | devtools, 288 | text 289 | } = req.body 290 | // console.log('post /', req.url, locale, devtools) 291 | console.log( 292 | 'post /', 293 | url, 294 | devtools && devtools.length ? 'devtools exist' : 'devtools none' 295 | ) 296 | 297 | const components = STORE.get(url) || { paths: new Set() } 298 | STORE.set(url, components) 299 | 300 | meta && setComponentPath(meta, components) 301 | added && setComponentPath(added, components) 302 | removed && setComponentPath(removed, components) 303 | 304 | const l10n: AnalisysLocalization = STORE2.get(url) || { 305 | url, 306 | components: new Map() 307 | } 308 | analysysDevTools(l10n, devtools || []) 309 | 310 | const data = await screenshot(url) 311 | if (data) { 312 | components.screenshot = data 313 | l10n.screenshot = data 314 | 315 | const d = await recognize(data) 316 | // console.log(d) 317 | components.recognize = d.data 318 | l10n.recoganize = d.data 319 | detectWithDevTools(l10n, d.data) 320 | // console.log('detect', l10n.detecting) 321 | // serializeDetecting(l10n) 322 | } 323 | 324 | if (!STORE2.has(url)) { 325 | STORE2.set(url, l10n) 326 | } 327 | 328 | res.status(200).json({ 329 | url, 330 | DOMText: text, 331 | paths: [...components.paths], 332 | screenshot: components.screenshot, 333 | recognize: components.recognize, 334 | detecting: [...l10n.detecting!.values()], 335 | notyet: [...l10n.notyet!.values()] 336 | }) 337 | }) 338 | 339 | app.listen(PORT, () => { 340 | console.log( 341 | `backend for dev clawler listening at ${chalk.cyan( 342 | `http://localhost:${PORT}` 343 | )}` 344 | ) 345 | }) 346 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/backend/phrase.ts: -------------------------------------------------------------------------------- 1 | import { default as _fs, promises as fs } from 'fs' 2 | import fetch from 'node-fetch' 3 | import FormData from 'form-data' 4 | import { 5 | Configuration, 6 | KeysApi, 7 | ProjectsApi, 8 | LocalesApi, 9 | UploadsApi, 10 | ScreenshotsApi, 11 | ScreenshotMarkersApi 12 | } from 'phrase-js' 13 | import { config as dotEnvConfig } from 'dotenv' 14 | 15 | import type { Project, Locale, Screenshot } from 'phrase-js' 16 | import type { AnalisysLocalization } from './index' 17 | 18 | const globalAny: any = global 19 | globalAny.window = { fetch } 20 | globalAny.fetch = fetch 21 | globalAny.FormData = FormData 22 | globalAny.atob = (a: string) => Buffer.from(a, 'base64').toString('binary') 23 | globalAny.btoa = (b: ArrayBuffer | SharedArrayBuffer) => 24 | Buffer.from(b).toString('base64') 25 | 26 | const LOCAL_ENV = dotEnvConfig({ path: './.env.local' }).parsed || {} 27 | 28 | export type PhraseInfo = { 29 | conf: Configuration 30 | project: Project 31 | locale?: Locale 32 | } 33 | 34 | export async function getPhraseInfo() { 35 | const info = {} as PhraseInfo 36 | info.conf = new Configuration({ 37 | apiKey: `Bearer ${LOCAL_ENV['PHRASE_API_TOKEN']}` 38 | }) 39 | const project = new ProjectsApi(info.conf) 40 | const projects = await project.projectsList({ page: 1, perPage: 25 }) 41 | info.project = projects[0] 42 | const locales = await getLocales(info) 43 | info.locale = locales.find(l => l.code! == 'en-US') 44 | // const formatAPI = await new FormatsApi(info.conf) 45 | // console.log(await formatAPI.formatsList({})) 46 | return info 47 | } 48 | 49 | export async function getKeys(info: PhraseInfo) { 50 | const keyAPI = new KeysApi(info.conf) 51 | return await keyAPI.keysList({ projectId: info.project.id! }) 52 | } 53 | 54 | export async function getLocales(info: PhraseInfo) { 55 | const localesAPI = new LocalesApi(info.conf) 56 | return await localesAPI.localesList({ projectId: info.project.id! }) 57 | } 58 | 59 | export async function uploadResources(info: PhraseInfo) { 60 | const uploadAPI = new UploadsApi(info.conf) 61 | return await uploadAPI.uploadsList({ projectId: info.project.id! }) 62 | } 63 | 64 | export async function uploadResource(info: PhraseInfo, filepath: string) { 65 | const uploadAPI = new UploadsApi(info.conf) 66 | let ret = {} 67 | try { 68 | ret = await uploadAPI.uploadCreate({ 69 | projectId: info.project.id!, 70 | localeId: info.locale!.id!, 71 | fileFormat: 'nested_json', 72 | file: _fs.createReadStream(filepath) as any // cannot work Blob ... 73 | }) 74 | } catch (e) { 75 | console.error(e) 76 | } 77 | } 78 | 79 | export async function uploadScreenshot( 80 | info: PhraseInfo, 81 | l10n: AnalisysLocalization, 82 | filepath: string 83 | ) { 84 | try { 85 | console.log('url', l10n.url, filepath) 86 | const form = new FormData() 87 | // form.append('Content-Type', 'application/octet-stream') 88 | form.append('name', 'screenshot') 89 | form.append('description', l10n.url) 90 | form.append('filename', _fs.createReadStream(filepath)) 91 | const headers = { 92 | Authorization: info.conf.apiKey!('Authorization') 93 | } 94 | const reqURL = `https://api.phrase.com/v2/projects/${info.project 95 | .id!}/screenshots` 96 | const req = await fetch(reqURL, { 97 | headers, 98 | method: 'POST', 99 | body: form 100 | }) 101 | const screenshot = (await req.json()) as Screenshot 102 | console.log('upload screenshot data', screenshot) 103 | const keys = await getKeys(info) 104 | const detecting = [...l10n.detecting!.values()] 105 | const screenshotMarkersApi = new ScreenshotMarkersApi(info.conf) 106 | const getKey = (name: string) => keys.find(k => k.name! === name) 107 | for (const { lineOrWord, devtool } of detecting) { 108 | const key = getKey(devtool.key) 109 | if (key) { 110 | const ret = await screenshotMarkersApi.screenshotMarkerCreate({ 111 | projectId: info.project.id!, 112 | screenshotId: screenshot.id!, 113 | screenshotMarkerCreateParameters: { 114 | keyId: key.id!, 115 | presentation: JSON.stringify({ 116 | x: lineOrWord.bbox.x0, 117 | y: lineOrWord.bbox.y0, 118 | w: lineOrWord.bbox.x1 - lineOrWord.bbox.x0, 119 | h: lineOrWord.bbox.y1 - lineOrWord.bbox.y0 120 | }) 121 | } 122 | }) 123 | console.log('marker', key, ret) 124 | } 125 | } 126 | // NOTE: cannot work the below code ... 127 | // const screenshotAPI = new ScreenshotsApi(info.conf) 128 | // const screenshot = await screenshotAPI.screenshotCreate({ 129 | // projectId: info.project.id!, 130 | // screenshotCreateParameters: { 131 | // name: 'screenshot', 132 | // description: 'desc', //l10n.url, 133 | // filename: _fs.createReadStream(filepath) as any 134 | // } 135 | // }) 136 | } catch (e) { 137 | console.error(e, e.headers) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/backend/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intlify/devtools/2b3d932564dcd7c94af3bd02a32ba79a736a7e4c/packages/clawler/src/dev/backend/tmp/.gitkeep -------------------------------------------------------------------------------- /packages/clawler/src/dev/backend/utils.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer' 2 | import Tesseract from 'tesseract.js' 3 | import path from 'path' 4 | 5 | const headless = true 6 | const slowMo = 10 7 | const width = 1280 8 | const height = 800 9 | const args = ['--start-fullscreen', '--disable-infobars', '--incognito'] 10 | 11 | function delay(ms: number) { 12 | return new Promise(resolve => { 13 | setTimeout(resolve, ms) 14 | }) 15 | } 16 | 17 | let capturing = false 18 | 19 | export async function screenshot(url: string, filepath?: string, ms = 0) { 20 | let browser = null 21 | if (capturing) { 22 | return null 23 | } 24 | 25 | try { 26 | capturing = true 27 | browser = await puppeteer.launch({ headless, args }) 28 | const page = await browser.newPage() 29 | await page.setViewport({ width, height, deviceScaleFactor: 2 }) 30 | page.on('console', msg => console.log(`[puppeteer]:`, msg.text())) 31 | await page.goto(`${url}?screenshot=true`, { waitUntil: 'networkidle2' }) 32 | if (ms > 0) { 33 | await delay(ms) 34 | } 35 | let options = null 36 | if (filepath) { 37 | options = { type: 'png', path: filepath } 38 | } else { 39 | options = { encoding: 'base64' } 40 | } 41 | const data = await page.screenshot(options as any) 42 | return filepath ? null : `data:image/png;base64,${data}` 43 | } finally { 44 | if (browser) { 45 | browser.close() 46 | } 47 | capturing = false 48 | } 49 | } 50 | 51 | export async function recognize(image: string) { 52 | const worker = await Tesseract.createWorker() 53 | await worker.load() 54 | await worker.loadLanguage('jpn+eng') 55 | await worker.initialize('jpn+eng') 56 | await worker.setParameters({ 57 | // tessedit_pageseg_mode: Tesseract.PSM.AUTO, 58 | tessedit_ocr_engine_mode: Tesseract.OEM.LSTM_ONLY, 59 | preserve_interword_spaces: '1' 60 | }) 61 | const data = await worker.recognize(image) 62 | await worker.terminate() 63 | return data 64 | } 65 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/frontend/App.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 98 | 99 | 119 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/frontend/components/Hello.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/frontend/components/Meta.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 110 | 111 | 117 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/frontend/components/Off.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/frontend/components/On.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/frontend/i18n.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import messages from '@intlify/vite-plugin-vue-i18n/messages' 3 | // import enUS from './locales/en-US.json' 4 | // import jaJP from './locales/ja-JP.json' 5 | 6 | console.log('load messages', messages) 7 | // console.log('load messages', enUS, jaJP) 8 | 9 | const i18n = createI18n({ 10 | legacy: false, 11 | locale: 'en-US', 12 | // locale: 'ja-JP', 13 | fallbackLocale: 'en-US', 14 | globalInjection: true, 15 | // messages: { 16 | // 'en-US': enUS, 17 | // 'ja-JP': jaJP 18 | // } 19 | messages 20 | }) 21 | 22 | export default i18n 23 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/frontend/locales/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Your web application", 3 | "banana": "How dou you have banana? I have @:bananaItems", 4 | "bananaItems": "no bananas | {n} banana | {n} bananas", 5 | "quantity": "Quantity", 6 | "pages": { 7 | "home": "Home", 8 | "about": "About" 9 | }, 10 | "comoponents": { 11 | "hello": { 12 | "greeting": "Hello {name} !" 13 | }, 14 | "off": "Off", 15 | "on": "On" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/frontend/locales/ja-JP.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "あなたのWebアプリケーション", 3 | "pages": { 4 | "home": "ホーム", 5 | "about": "あばうと" 6 | }, 7 | "comoponents": { 8 | "hello": { 9 | "greeting": "こんにちは {name} !" 10 | }, 11 | "off": "無効", 12 | "on": "有効" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/frontend/main.ts: -------------------------------------------------------------------------------- 1 | import { hook } from '../../hook' 2 | import { createApp, nextTick as waitForFullyMount } from 'vue' 3 | import App from './App.vue' 4 | import router from './router' 5 | import { mixin } from './mixin' 6 | 7 | import { default as clawl } from '../../main' 8 | const doClawl = clawl(hook) 9 | 10 | function checkScreenshotRequest() { 11 | const url = new URL(window.location.href) 12 | const params = new URLSearchParams(url.searchParams) 13 | console.log( 14 | 'checkStreeshot', 15 | url, 16 | window.location.href, 17 | params.has('screenshot') 18 | ) 19 | return params.has('screenshot') 20 | } 21 | 22 | ;(async () => { 23 | const { default: i18n } = await import('./i18n') 24 | createApp(App).mixin(mixin).use(router).use(i18n).mount('#app') 25 | await waitForFullyMount() 26 | if (checkScreenshotRequest()) { 27 | return 28 | } 29 | await doClawl(document.body) 30 | })() 31 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/frontend/mixin.ts: -------------------------------------------------------------------------------- 1 | export const mixin = { 2 | mounted(this: any) { 3 | const { _, $el } = this 4 | // console.log('mounted', _, $el, $el.nodeType) 5 | if (_ && _.type && _.type.__INTLIFY_META__ && $el) { 6 | $el.__INTLIFY_META__ = _.type.__INTLIFY_META__ 7 | if ( 8 | $el.nodeType === Node.TEXT_NODE && 9 | $el.nextSibling && 10 | $el.nextSibling.nodeType === Node.ELEMENT_NODE 11 | ) { 12 | // for fragment 13 | const { nextSibling: nextEl } = $el 14 | nextEl.setAttribute('data-intlify', _.type.__INTLIFY_META__) 15 | // $el.__INTLIFY_META__ = nextEl.__INTLIFY_META__ = _.type.__INTLIFY_META__ 16 | } else { 17 | $el.setAttribute('data-intlify', _.type.__INTLIFY_META__) 18 | // $el.__INTLIFY_META__ = _.type.__INTLIFY_META__ 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/frontend/pages/About.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/frontend/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | -------------------------------------------------------------------------------- /packages/clawler/src/dev/frontend/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import Home from './pages/Home.vue' 3 | import About from './pages/About.vue' 4 | 5 | import type { RouteRecordRaw } from 'vue-router' 6 | 7 | const routes: RouteRecordRaw[] = [ 8 | { 9 | path: '/', 10 | name: 'home', 11 | component: Home 12 | }, 13 | { 14 | path: '/about', 15 | name: 'about', 16 | component: About 17 | }, 18 | { 19 | path: '/:pathMatch(.*)*', 20 | redirect: () => `/` 21 | } 22 | ] 23 | 24 | export default createRouter({ 25 | history: createWebHistory(), 26 | routes 27 | }) 28 | -------------------------------------------------------------------------------- /packages/clawler/src/helper.ts: -------------------------------------------------------------------------------- 1 | export function isEmpty(items?: unknown[]) { 2 | return items && items.length === 0 3 | } 4 | 5 | export function getEndPoint() { 6 | return import.meta.env.VITE_BASE_ENDPOINT as string 7 | } 8 | 9 | export function extractDomContent( 10 | originalNode: any, 11 | options: Record = {} 12 | ) { 13 | const mainNode = originalNode.cloneNode(true) 14 | const allOriginalNodes = originalNode.querySelectorAll('*') 15 | const allClonedNodes = mainNode.querySelectorAll('*') 16 | 17 | // Apply computed styles as inline CSS for every node, as window.getComputedStyle isn't available outside the DOM 18 | for (let i = 0; i < allOriginalNodes.length; i++) { 19 | allClonedNodes[i].setAttribute( 20 | 'style', 21 | window.getComputedStyle(allOriginalNodes[i]).cssText 22 | ) 23 | } 24 | 25 | // Get rid of unwanted elements for copy text 26 | for (const unwantedNode of mainNode.querySelectorAll( 27 | 'script, style, noscript, code' 28 | )) { 29 | unwantedNode.remove() 30 | } 31 | 32 | // Prevent
from causing stuck-together words 33 | for (const brNode of mainNode.querySelectorAll('br')) { 34 | brNode.outerHTML = 35 | brNode.nextSibling && 36 | brNode.nextSibling.nodeValue && 37 | brNode.nextSibling.nodeValue.trim().length 38 | ? ' ' 39 | : '\n' 40 | } 41 | 42 | // Replace images with their alt text if they have one 43 | for (const imgNode of mainNode.querySelectorAll('img[alt]:not([alt=""])')) { 44 | imgNode.outerHTML = '\n' + imgNode.alt + '\n' 45 | } 46 | 47 | // Flex, block or grid display links with that only contain text can most likely be on their own line 48 | for (const linkNode of mainNode.querySelectorAll('a')) { 49 | const display = linkNode.style.display.toLowerCase() 50 | if ( 51 | display == 'block' || 52 | display.indexOf('flex') != -1 || 53 | display.indexOf('grid') != -1 54 | ) { 55 | if ( 56 | ![...linkNode.childNodes].filter(node => { 57 | return node.nodeName != '#text' 58 | }).length 59 | ) { 60 | linkNode.innerHTML = '\n\n' + linkNode.innerHTML 61 | } 62 | } 63 | } 64 | 65 | // Flex childs are rarely words forming a sentence: break them apart 66 | for (const node of mainNode.querySelectorAll('*')) { 67 | if (node.style.display.toLowerCase().indexOf('flex') != -1) { 68 | for (const child of node.children) { 69 | child.innerHTML = '\n\n' + child.innerHTML 70 | } 71 | } 72 | } 73 | 74 | // Simple fix for minified HTML 75 | mainNode.innerHTML = mainNode.innerHTML.replace(/> <') 76 | 77 | // Make sure headings are on their own lines - they should be "self-sufficient" 78 | mainNode.innerHTML = mainNode.innerHTML.replace( 79 | //g, 80 | '\n\n' 81 | ) 82 | console.log( 83 | 'innnerHTML', 84 | mainNode.innerHTML, 85 | mainNode.innerText, 86 | mainNode.textContent 87 | ) 88 | 89 | // Home stretch... 90 | const rawContent = (mainNode.innerText || mainNode.textContent || '') 91 | .replace(/(\s{2,})([A-Z0-9])/g, '$1\n$2') // split blocks that seem to contain multiple sentences or standalone blocks 92 | .replace(/\s{3,}/g, '\n') // break everything into single line blocks 93 | .replace(/\n.{1,3}\n/g, '\n') // remove tiny words or tokens that are on their own 94 | .replace(/ {2,}/g, ' ') // replace multiple spaces by a single one 95 | .replace(/^\s(.+)$/gm, '$1') // remove spaces at the beginning of lines 96 | 97 | if (options.removeDuplicates || false) { 98 | // Get an array of strings without duplicates via the Set constructor and spread operator 99 | const contentStrings = [...new Set(rawContent.split('\n'))] 100 | 101 | if (options.returnAsArray || false) { 102 | return contentStrings 103 | } 104 | 105 | return contentStrings.join('\n') 106 | } else if (options.returnAsArray || false) { 107 | return rawContent.split('\n') 108 | } 109 | 110 | return rawContent 111 | } 112 | -------------------------------------------------------------------------------- /packages/clawler/src/hook.ts: -------------------------------------------------------------------------------- 1 | import { getGlobalThis, createEmitter } from '@intlify/shared' 2 | 3 | import type { Emittable } from '@intlify/shared' 4 | import type { 5 | IntlifyDevToolsEmitterHooks, 6 | IntlifyDevToolsHookPayloads 7 | } from '@intlify/devtools-if' 8 | 9 | export interface IntlifyHook { 10 | i18nPayload: any[] 11 | translatePayload: any[] 12 | emitter: Emittable 13 | clearI18nPayload(): void 14 | clearTranslatePayload(): void 15 | } 16 | 17 | function createHook() { 18 | const target = getGlobalThis() 19 | if (target.__INTLIFY_DEVTOOLS_GLOBAL_HOOK__) { 20 | return target.__INTLIFY_DEVTOOLS_GLOBAL_HOOK__ 21 | } 22 | 23 | const emitter = createEmitter() 24 | target.__INTLIFY_DEVTOOLS_GLOBAL_HOOK__ = emitter 25 | 26 | let _i18nPayload: any[] = [] 27 | let _translatePayload: any[] = [] 28 | 29 | // TODO: type errors 30 | // @ts-ignore 31 | emitter.on( 32 | 'i18n:init', 33 | (payload: IntlifyDevToolsHookPayloads['i18n:init']) => { 34 | console.log('i18n:init', payload) 35 | _i18nPayload.push(payload) 36 | } 37 | ) 38 | 39 | // TODO: type errors 40 | // @ts-ignore 41 | emitter.on( 42 | 'function:translate', 43 | (payload: IntlifyDevToolsHookPayloads['function:translate']) => { 44 | console.log('function:translate', payload) 45 | _translatePayload.push(payload) 46 | } 47 | ) 48 | 49 | function clearI18nPayload() { 50 | _i18nPayload = [] 51 | console.log('clear i18n payload', _i18nPayload) 52 | } 53 | 54 | function clearTranslatePayload() { 55 | _translatePayload = [] 56 | console.log('clear translate payload', _translatePayload) 57 | } 58 | 59 | return { 60 | get i18nPayload() { 61 | return _i18nPayload 62 | }, 63 | get translatePayload() { 64 | return _translatePayload 65 | }, 66 | emitter, 67 | clearI18nPayload, 68 | clearTranslatePayload 69 | } 70 | } 71 | 72 | export const hook = createHook() 73 | -------------------------------------------------------------------------------- /packages/clawler/src/main.ts: -------------------------------------------------------------------------------- 1 | import { attachWorker } from '@intlify/worker-dom/dist/lib/main' 2 | import { Locale } from 'vue-i18n' 3 | import WorkerDOM from './worker?worker' 4 | import { isEmpty, getEndPoint, extractDomContent } from './helper' 5 | 6 | import type { IntlifyHook } from './hook' 7 | 8 | export default function clawl(hook: IntlifyHook, Worker?: any) { 9 | return async (el: HTMLElement) => { 10 | const worker = await attachWorker( 11 | el, 12 | Worker ? new Worker() : new WorkerDOM() 13 | ) 14 | console.log('clawler run!', el) 15 | 16 | const observer = observeDOM(worker, hook) 17 | observer.observe(el, { childList: true, subtree: true }) 18 | console.log('observe!') 19 | 20 | console.log('... ready worker') 21 | await worker.callFunction('ready') 22 | console.log('... done worker!') 23 | 24 | const { url, meta, text } = await worker.callFunction( 25 | 'walkElements', 26 | window.location.href 27 | ) 28 | console.log('collect', meta, text) 29 | console.log('page url', url) 30 | 31 | // const canvas = await html2canvas(document.body) 32 | 33 | // const i18nGlobal = i18n.global as Composer 34 | // const body = { 35 | // url, 36 | // meta, 37 | // text, 38 | // locale: 'en', // i18nGlobal.locale.value, 39 | // // screenshot: canvas.toDataURL(), 40 | // timestamp: new Date().getTime() 41 | // } 42 | // const res = await worker.callFunction('pushMeta', getEndPoint(), body) 43 | // console.log('backend res clawl', res, import.meta.env) 44 | } 45 | } 46 | 47 | type MutationRecord = { 48 | url: string 49 | removed: string[] 50 | added: string[] 51 | text: string[] 52 | locale?: Locale 53 | devtools?: any[] 54 | timestamp?: number 55 | } 56 | 57 | function observeDOM(worker: any, hook: IntlifyHook) { 58 | const observer = new MutationObserver(async mutations => { 59 | const body: MutationRecord = { 60 | url: window.location.href, 61 | removed: [], 62 | added: [], 63 | text: [], 64 | locale: 'en' 65 | } 66 | const textSet = new Set() 67 | 68 | mutations.forEach(mutation => { 69 | console.log('mutaion observer', mutation) 70 | mutation.addedNodes.forEach(node => { 71 | walkElements('added', node, mutation.target, body) 72 | walkTargetElement(mutation.target, textSet) 73 | console.log( 74 | 'extract-dom-content', 75 | extractDomContent(mutation.target, { returnAsArray: true }) 76 | ) 77 | }) 78 | mutation.removedNodes.forEach(node => 79 | walkElements('removed', node, mutation.target, body) 80 | ) 81 | }) 82 | 83 | body.text = [...textSet] 84 | body.timestamp = Date.now() 85 | body.devtools = hook.translatePayload 86 | console.log('text set', body.text) 87 | 88 | if (isEmpty(body.removed) && isEmpty(body.added) && isEmpty(body.text)) { 89 | return 90 | } 91 | 92 | // const canvas = await html2canvas(document.body) 93 | // body.screenshot = canvas.toDataURL() 94 | 95 | console.log('hook payloads', hook.i18nPayload, hook.translatePayload) 96 | 97 | console.log('send body ...', body) 98 | const res = await worker.callFunction('pushMeta', getEndPoint(), body) 99 | console.log('backend res observeDOM', res, import.meta.env) 100 | 101 | hook.clearTranslatePayload() 102 | }) 103 | 104 | return observer 105 | } 106 | 107 | function isDOMElementNode(node: Node): node is Element { 108 | return node.nodeType === Node.ELEMENT_NODE 109 | } 110 | 111 | function walkTargetElement(node: Node, text: Set) { 112 | node.childNodes.forEach(node => { 113 | if (node.nodeType === Node.TEXT_NODE && node.textContent) { 114 | text.add(node.textContent) 115 | } 116 | if (node.childNodes) { 117 | walkTargetElement(node, text) 118 | } 119 | }) 120 | } 121 | 122 | function walkElements( 123 | type: 'added' | 'removed', 124 | node: Node, 125 | target: Node, 126 | body: MutationRecord 127 | ) { 128 | const { __INTLIFY_META__ } = node as any 129 | const metaInfo = body[type] 130 | if (isDOMElementNode(target)) { 131 | // console.log('target position', type, target, target.getBoundingClientRect()) 132 | if (isDOMElementNode(node)) { 133 | // console.log('node position', type, node, node.getBoundingClientRect()) 134 | } else { 135 | // console.log('node', node) 136 | } 137 | } 138 | __INTLIFY_META__ && metaInfo.push(__INTLIFY_META__) 139 | node.childNodes.forEach(node => walkElements(type, node, target, body)) 140 | // target.childNodes.forEach(node => { 141 | // console.log('enum', node.nodeType, (node.nodeType === Node.ELEMENT_NODE ? (node as Element).tagName : ''), node.textContent, node.textContent?.length) 142 | // }) 143 | } 144 | -------------------------------------------------------------------------------- /packages/clawler/src/types.ts: -------------------------------------------------------------------------------- 1 | export type MetaInfo = string[] 2 | -------------------------------------------------------------------------------- /packages/clawler/src/worker.ts: -------------------------------------------------------------------------------- 1 | import { ready, exportFunctions } from '@intlify/worker-dom/dist/lib/worker' 2 | import type { MetaInfo } from './types' 3 | 4 | let _metaInfo: MetaInfo | null = null 5 | 6 | let _resolve: Function | null = null // eslint-disable-line @typescript-eslint/ban-types 7 | const _ready = new Promise(resolve => { 8 | _resolve = resolve 9 | }) 10 | 11 | async function delay(ms: number) { 12 | return new Promise(resolve => { 13 | setTimeout(resolve, ms) 14 | }) 15 | } 16 | 17 | const exportingFunctions = { 18 | async ready(): Promise { 19 | await _ready 20 | }, 21 | async getIntlifyMetaInfo(): Promise { 22 | if (_metaInfo != null) { 23 | return _metaInfo 24 | } 25 | _metaInfo = [] 26 | walkElements(document.body, _metaInfo, []) 27 | return _metaInfo 28 | }, 29 | async pushMeta( 30 | endpoint: string, 31 | body: Record 32 | ): Promise> { 33 | console.log('worker:pushMeta', endpoint, body) 34 | return ( 35 | await fetch(endpoint, { 36 | method: 'post', 37 | mode: 'cors', 38 | headers: { 39 | Accept: 'application/json', 40 | 'Content-Type': 'application/json' 41 | }, 42 | body: JSON.stringify(body) 43 | }) 44 | ).json() 45 | }, 46 | async walkElements( 47 | url?: string 48 | ): Promise<{ url?: string; meta: MetaInfo; text?: string[] }> { 49 | const metaInfo: MetaInfo = [] 50 | const text: string[] = [] 51 | walkElements(document.body, metaInfo, text) 52 | _metaInfo = metaInfo 53 | return { url, meta: metaInfo, text } 54 | } 55 | } 56 | 57 | exportFunctions(exportingFunctions) 58 | 59 | function getIntlifyMetaData(attributes: Attr[]): string { 60 | const attr = attributes.find(({ name }) => name === 'data-intlify') 61 | return attr ? attr.value : '' 62 | } 63 | 64 | function hasCharacters(target: string): boolean { 65 | return !!target.replace(/[\s\t\r\n]+/g, '').length 66 | } 67 | 68 | function walkElements(node: Node, metaInfo: MetaInfo, text: string[]) { 69 | // console.log('id, __INTLIFY__META__', node.nodeName, node.__INTLIFY_META__) 70 | const { __INTLIFY_META__ } = node as any 71 | __INTLIFY_META__ && metaInfo.push(__INTLIFY_META__) 72 | node.childNodes.forEach((node: Node) => { 73 | // console.log('worker enum', node.nodeType, node.textContent, hasCharacters(node.textContent!)) 74 | node.nodeType === 3 && 75 | node.textContent && 76 | hasCharacters(node.textContent) && 77 | text.push(node.textContent) 78 | walkElements(node, metaInfo, text) 79 | }) 80 | } 81 | 82 | ;(async () => { 83 | await ready 84 | _metaInfo = [] 85 | // TODO: 86 | // await delay(3000) 87 | // console.log('... worker dom initialized !!') 88 | // @ts-ignore 89 | _resolve && _resolve() 90 | // await walkElement(document.body, _metaInfo) 91 | })() 92 | -------------------------------------------------------------------------------- /packages/clawler/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "lib": ["esnext", "dom"], 11 | "types": ["vite/client", "@intlify/vite-plugin-vue-i18n/client"], 12 | "plugins": [{ "name": "@vuedx/typescript-plugin-vue" }] 13 | }, 14 | "include": [ 15 | "src/**/*.ts", 16 | "src/**/*.d.ts", 17 | "src/**/*.tsx", 18 | "src/**/*.vue" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/clawler/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import vueI18n from '@intlify/vite-plugin-vue-i18n' 4 | import intlifyVue from '@intlify/vite-plugin-vue-i18n/lib/injection' 5 | import path from 'path' 6 | import { generateSecret, encrypt } from '@intlify-devtools/shared' 7 | import { config as dotEnvConfig } from 'dotenv' 8 | 9 | const LOCAL_ENV = 10 | dotEnvConfig({ path: path.resolve(__dirname, './.env.local') }).parsed || {} 11 | // @ts-ignore 12 | const SECRET = 13 | LOCAL_ENV.INTLIFY_META_SECRET || 14 | process.env.INTLIFY_META_SECRET || 15 | generateSecret() 16 | const BACKEND_PORT = process.env.PORT || 4000 17 | 18 | // for vite serve 19 | const serveConfig = defineConfig({ 20 | plugins: [ 21 | vue(), 22 | vueI18n({ 23 | include: path.resolve(__dirname, './src/dev/frontend/locales/**') 24 | }), 25 | intlifyVue({ 26 | __INTLIFY_META__: (a1, a2) => { 27 | const { iv, encryptedData } = encrypt(SECRET, a1) 28 | return `${iv}$${encryptedData}` 29 | } 30 | }) 31 | ], 32 | server: { 33 | port: 3200, 34 | proxy: { 35 | '/bend': { 36 | target: `http://localhost:${BACKEND_PORT}`, 37 | changeOrigin: true, 38 | rewrite: path => path.replace(/^\/bend/, '/') 39 | } 40 | } 41 | } 42 | }) 43 | 44 | // for vite build 45 | const buildConfig = defineConfig({ 46 | publicDir: './dist', 47 | build: { 48 | lib: { 49 | entry: path.resolve(__dirname, 'src/main.ts'), 50 | formats: ['es'] 51 | } 52 | } 53 | }) 54 | 55 | // https://vitejs.dev/config/ 56 | export default ({ command }) => 57 | command === 'build' ? buildConfig : serveConfig 58 | -------------------------------------------------------------------------------- /packages/devtools/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 kazuya kawaguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/devtools/README.md: -------------------------------------------------------------------------------- 1 | # @intlify/devtools 2 | 3 | ## :copyright: License 4 | 5 | [MIT](http://opensource.org/licenses/MIT) 6 | -------------------------------------------------------------------------------- /packages/devtools/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Intliify Devtools Dev 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/devtools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@intlify/devtools", 3 | "version": "0.0.0", 4 | "bugs": { 5 | "url": "https://github.com/intlify/devtools/issues" 6 | }, 7 | "dependencies": { 8 | "@intlify-devtools/shared": "0.0.0", 9 | "@intlify/worker-dom": "^1.1.0", 10 | "html2canvas": "^1.0.0-rc.7", 11 | "tesseract.js": "^2.1.4", 12 | "vue": "^3.0.11" 13 | }, 14 | "engines": { 15 | "node": ">= 12" 16 | }, 17 | "exports": { 18 | ".": { 19 | "import": "dist/devtools.es.js", 20 | "require": "dist/devtools.umd.js" 21 | } 22 | }, 23 | "files": [ 24 | "dist/devtools.es.js", 25 | "dist/devtools.umd.js", 26 | "dist/styles.css" 27 | ], 28 | "homepage": "https://github.com/intlify/devtools/tree/master/packages/devtools#readme", 29 | "jsdelivr": "dist/devtools.umd.js", 30 | "main": "dist/devtools.umd.js", 31 | "module": "dist/devtools.es.js", 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/intlify/devtools.git", 35 | "directory": "packages/devtools" 36 | }, 37 | "scripts": { 38 | "build": "vite build", 39 | "dev": "vite", 40 | "lint": "vuedx-typecheck .", 41 | "serve": "vite preview" 42 | }, 43 | "sideEffects": false, 44 | "unpkg": "dist/devtools.umd.js" 45 | } 46 | -------------------------------------------------------------------------------- /packages/devtools/src/App.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 64 | 65 | 74 | -------------------------------------------------------------------------------- /packages/devtools/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intlify/devtools/2b3d932564dcd7c94af3bd02a32ba79a736a7e4c/packages/devtools/src/assets/.gitkeep -------------------------------------------------------------------------------- /packages/devtools/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /packages/devtools/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Component, createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | async function mount(Entry: Component, el?: string) { 5 | if (el) { 6 | const app = createApp(App) 7 | app.mixin({ 8 | mounted() { 9 | const { _, $el } = this 10 | if (_ && _.type && _.type.__INTLIFY_META__ && $el) { 11 | if ( 12 | $el.nodeType === Node.TEXT_NODE && 13 | $el.nextSibling && 14 | $el.nextSibling.nodeType === Node.ELEMENT_NODE 15 | ) { 16 | // for fragment 17 | const { nextSibling: nextEl } = $el 18 | nextEl.setAttribute('data-intlify', _.type.__INTLIFY_META__) 19 | } else { 20 | $el.setAttribute('data-intlify', _.type.__INTLIFY_META__) 21 | } 22 | } 23 | } 24 | }) 25 | app.mount(el) 26 | // window.addEventListener('mousemove', (ev) => { 27 | // console.log('ev', ev.offsetX, ev.offsetY, ev.clientX, ev.clientY) 28 | // }) 29 | return { app } 30 | } else { 31 | const root = document.createElement('div') 32 | root.id = 'intlify-app' 33 | document.body.appendChild(root) 34 | const app = createApp(Entry).mount(root) 35 | return { app, root } 36 | } 37 | } 38 | 39 | ;(async () => { 40 | if (import.meta.env.DEV) { 41 | mount(App, '#app') 42 | } else if (import.meta.env.PROD) { 43 | if (document.readyState !== 'loading') { 44 | mount(App) 45 | return 46 | } 47 | document.addEventListener('DOMContentLoaded', async () => { 48 | console.log('load 3rd part script') 49 | mount(App) 50 | }) 51 | } 52 | })().catch((e: Error) => { 53 | console.error(e) 54 | }) 55 | -------------------------------------------------------------------------------- /packages/devtools/src/types.ts: -------------------------------------------------------------------------------- 1 | export type MetaInfo = string[] 2 | -------------------------------------------------------------------------------- /packages/devtools/src/worker.ts: -------------------------------------------------------------------------------- 1 | import { ready, exportFunctions } from '@intlify/worker-dom/dist/lib/worker' 2 | import type { MetaInfo } from './types' 3 | 4 | let _metaInfo: MetaInfo | null = null 5 | 6 | async function getIntlifyMetaInfo(): Promise { 7 | if (_metaInfo != null) { 8 | return _metaInfo 9 | } 10 | _metaInfo = [] 11 | walkElement(document.body, _metaInfo) 12 | return _metaInfo 13 | } 14 | 15 | exportFunctions([getIntlifyMetaInfo]) 16 | 17 | function getIntlifyMetaData(attributes: Attr[]): string { 18 | const attr = attributes.find(({ name }) => name === 'data-intlify') 19 | return attr ? attr.value : '' 20 | } 21 | 22 | function walkElement(node: Node, metaInfo: MetaInfo) { 23 | node.childNodes.forEach(node => { 24 | // console.log('id, __INTLIFY__META__', (node as HTMLElement).id, (node as any).attributes, node) 25 | if (node.nodeType === 1 && 'attributes' in node) { 26 | const element = (node as unknown) as Element 27 | const value = getIntlifyMetaData( 28 | (element.attributes as unknown) as Attr[] 29 | ) 30 | console.log('metainfo', value) 31 | value && metaInfo.push(value) 32 | } 33 | walkElement(node, metaInfo) 34 | }) 35 | } 36 | 37 | function createElement(msg: string, id: string): HTMLDivElement { 38 | const el = document.createElement('div') 39 | el.id = id 40 | el.textContent = msg 41 | return el 42 | } 43 | 44 | let counter = 1 45 | 46 | ;(async () => { 47 | await ready 48 | document.body.appendChild( 49 | createElement('The world', `container-${++counter}`) 50 | ) 51 | })() 52 | -------------------------------------------------------------------------------- /packages/devtools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "lib": ["esnext", "dom"], 11 | "types": ["vite/client"], 12 | "plugins": [{ "name": "@vuedx/typescript-plugin-vue" }] 13 | }, 14 | "include": [ 15 | "src/**/*.ts", 16 | "src/**/*.d.ts", 17 | "src/**/*.tsx", 18 | "src/**/*.vue" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/devtools/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import vueI18n from '@intlify/vite-plugin-vue-i18n' 4 | import intlifyVue from '@intlify/vite-plugin-vue-i18n/lib/injection' 5 | import path from 'path' 6 | import { generateSecret, encrypt } from '@intlify-devtools/shared' 7 | 8 | // @ts-ignore 9 | const secret = generateSecret() 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | plugins: [ 14 | vue(), 15 | vueI18n(), 16 | intlifyVue({ 17 | __INTLIFY_META__: (a1, a2) => { 18 | const { iv, encryptedData } = encrypt(secret, a1) 19 | return `${iv}$${encryptedData}` 20 | } 21 | }) 22 | ], 23 | publicDir: './dist', 24 | server: { 25 | port: 3100 26 | }, 27 | build: { 28 | lib: { 29 | entry: path.resolve(__dirname, 'src/main.ts'), 30 | name: 'IntlifyDevtools' 31 | } 32 | // rollupOptions: { 33 | // output: [ 34 | // { 35 | // file: 'intlify-devtools.es.js', 36 | // format: 'es' 37 | // }, 38 | // { 39 | // file: 'intlify-devtools.umd.js', 40 | // format: 'umd' 41 | // } 42 | // ] 43 | // } 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /packages/shared/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 kazuya kawaguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/shared/README.md: -------------------------------------------------------------------------------- 1 | # @intlify-devtools/shared 2 | 3 | ## :copyright: License 4 | 5 | [MIT](http://opensource.org/licenses/MIT) 6 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@intlify-devtools/shared", 3 | "description": "shared library for intlify devtools", 4 | "version": "0.0.0", 5 | "author": { 6 | "name": "kazuya kawaguchi", 7 | "email": "kawakazu80@gmail.com" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/intlify/devtools/issues" 11 | }, 12 | "dependencies": { 13 | "@babel/parser": "^7.13.13", 14 | "@vue/compiler-core": "^3.0.10", 15 | "@vue/compiler-sfc": "^3.0.10" 16 | }, 17 | "devDependencies": { 18 | "@babel/types": "^7.13.14", 19 | "@types/estree": "^0.0.47" 20 | }, 21 | "engines": { 22 | "node": ">= 12" 23 | }, 24 | "files": [ 25 | "dist" 26 | ], 27 | "homepage": "https://github.com/intlify/devtools/tree/main/packages/shared#readme", 28 | "license": "MIT", 29 | "main": "lib/index.js", 30 | "private": true, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/intlify/devtools.git", 34 | "directory": "packages/shared" 35 | }, 36 | "scripts": { 37 | "build": "tsc -b", 38 | "clean": "rm -rf lib/*.* tsconfig.tsbuildinfo", 39 | "watch": "tsc -b --watch" 40 | }, 41 | "types": "lib/index.d.ts" 42 | } 43 | -------------------------------------------------------------------------------- /packages/shared/src/babel.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '@babel/parser' 2 | import traverse, { NodePath } from '@babel/traverse' 3 | import { isArray } from '@intlify/shared' 4 | 5 | import { 6 | isIdentifier, 7 | isStringLiteral, 8 | isTemplateLiteral, 9 | isThisExpression, 10 | isMemberExpression, 11 | Identifier, 12 | StringLiteral, 13 | Node 14 | } from '@babel/types' 15 | 16 | export type I18nCallIdentifier = string 17 | export type I18nObjectIdentifier = string 18 | 19 | export const DEFAULT_I18N_CALL_IDENTIFITERS = ['t', '$t', 'tc', '$tc'] 20 | export const DEFAULT_I18N_OBJECT_IDENTIFITERS = ['this'] 21 | 22 | export interface TraverseI18nOptions { 23 | callIdentifiers?: I18nCallIdentifier[] 24 | objectIdentifiers?: I18nObjectIdentifier[] 25 | } 26 | 27 | function traverseCallableExpression( 28 | expression: Identifier | StringLiteral, 29 | argument: NodePath, 30 | keys: string[], 31 | callIdentifiers: I18nCallIdentifier[] 32 | ): void { 33 | if ( 34 | callIdentifiers.some( 35 | id => 36 | isIdentifier(expression, { name: id }) || 37 | isStringLiteral(expression, { value: id }) 38 | ) 39 | ) { 40 | if (!isArray(argument)) { 41 | if (isStringLiteral(argument.node)) { 42 | keys.push(argument.node.value) 43 | } else if ( 44 | isTemplateLiteral(argument.node) && 45 | argument.node.expressions.length === 0 46 | ) { 47 | // static only 48 | keys.push(argument.node.quasis[0].value.raw) 49 | } 50 | } 51 | } 52 | } 53 | 54 | export function traverseI18nCallExpression( 55 | source: string, 56 | options: TraverseI18nOptions = {} 57 | ): string[] { 58 | const callIdentifiers = 59 | options.callIdentifiers ?? DEFAULT_I18N_CALL_IDENTIFITERS 60 | const objectIdentifiers = 61 | options.objectIdentifiers ?? DEFAULT_I18N_OBJECT_IDENTIFITERS 62 | 63 | const ast = parse(source, { 64 | sourceType: 'module', 65 | plugins: [ 66 | 'topLevelAwait', 67 | 'jsx', // for vue-jsx 68 | 'typescript', 69 | 'decorators-legacy' // for vue-class-component 70 | ] 71 | }) 72 | 73 | const keys = [] as string[] 74 | traverse(ast, { 75 | CallExpression(path) { 76 | const keyArgument = path.get('arguments.0') 77 | if (isArray(keyArgument)) { 78 | return 79 | } 80 | 81 | if (isIdentifier(path.node.callee)) { 82 | traverseCallableExpression( 83 | path.node.callee, 84 | keyArgument, 85 | keys, 86 | callIdentifiers 87 | ) 88 | } else if (isMemberExpression(path.node.callee)) { 89 | const member = path.node.callee 90 | const { property, object } = member 91 | if ( 92 | (objectIdentifiers.includes('this') && isThisExpression(object)) || 93 | objectIdentifiers.some(id => isIdentifier(object, { name: id })) 94 | ) { 95 | if (isIdentifier(property) || isStringLiteral(property)) { 96 | traverseCallableExpression( 97 | property, 98 | keyArgument, 99 | keys, 100 | callIdentifiers 101 | ) 102 | } 103 | } 104 | } 105 | } 106 | }) 107 | return keys 108 | } 109 | -------------------------------------------------------------------------------- /packages/shared/src/crypt.ts: -------------------------------------------------------------------------------- 1 | import { 2 | randomBytes, 3 | scryptSync, 4 | createCipheriv, 5 | createDecipheriv 6 | } from 'crypto' 7 | 8 | const CRYPT_ALGO = 'aes-256-cbc' 9 | 10 | /** 11 | * Generate the random passprase 12 | * 13 | * @returns {string} A random passphrase with base64 14 | */ 15 | export function randomPass(): string { 16 | return randomBytes(32).toString('base64') 17 | } 18 | 19 | /** 20 | * Generate the random salt 21 | * 22 | * @returns {string} A random salt with base64 23 | */ 24 | export function randomSalt(): string { 25 | return randomBytes(16).toString('base64') 26 | } 27 | 28 | /** 29 | * Genearte a secret key 30 | * 31 | * @param {string} pass 32 byte password 32 | * @param {string} salt 16 byte salt 33 | * @returns {string} secrent hash key 34 | */ 35 | export function generateSecret(pass: string, salt: string): string { 36 | pass = pass || randomPass() 37 | salt = salt || randomSalt() 38 | return scryptSync(pass, salt, 32).toString('hex') 39 | } 40 | 41 | /** 42 | * Encrypt 43 | * 44 | * @param {string} secret A secret hash key 45 | * @param {string} data A target data 46 | * @returns {Object} An IV and encryped data that these are encdoed with base64 47 | */ 48 | export function encrypt( 49 | secret: string, 50 | data: string 51 | ): { iv: string; encryptedData: string } { 52 | // secret key from buffer 53 | const key = Buffer.from(secret, 'hex') 54 | // generate IV 55 | const iv = randomBytes(16) 56 | // create chiper 57 | const cipher = createCipheriv(CRYPT_ALGO, key, iv) 58 | // encrype data 59 | let encryptedData = cipher.update(data, 'utf-8', 'base64') 60 | encryptedData += cipher.final('base64') 61 | return { iv: iv.toString('base64'), encryptedData } 62 | } 63 | 64 | /** 65 | * Decrypt 66 | * 67 | * @param {string} secret A secret hash key 68 | * @param {string} iv An IV that encoded with base64 69 | * @param {string} encryptedData An encrypted data that encoded with base64 70 | * @returns {string} The decrypted data 71 | */ 72 | export function decrypt( 73 | secret: string, 74 | iv: string, 75 | encryptedData: string 76 | ): string { 77 | // secret key from buffer 78 | const key = Buffer.from(secret, 'hex') 79 | // IV from buffer 80 | const ivRaw = Buffer.from(iv, 'base64') 81 | // create dechiper 82 | const decipher = createDecipheriv(CRYPT_ALGO, key, ivRaw) 83 | // decrypt data 84 | let decryptedData = decipher.update(encryptedData, 'base64', 'utf-8') 85 | decryptedData += decipher.final('utf-8') 86 | return decryptedData 87 | } 88 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crypt' 2 | export * from './mixin' 3 | export * from './sfc' 4 | -------------------------------------------------------------------------------- /packages/shared/src/mixin.ts: -------------------------------------------------------------------------------- 1 | export const mixin = { 2 | mounted(this: any) { 3 | const { _, $el } = this 4 | if (_ && _.type && _.type.__INTLIFY_META__ && $el) { 5 | if ( 6 | $el.nodeType === Node.TEXT_NODE && 7 | $el.nextSibling && 8 | $el.nextSibling.nodeType === Node.ELEMENT_NODE 9 | ) { 10 | // for fragment 11 | const { nextSibling: nextEl } = $el 12 | nextEl.setAttribute('data-intlify', _.type.__INTLIFY_META__) 13 | } else { 14 | $el.setAttribute('data-intlify', _.type.__INTLIFY_META__) 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/shared/src/sfc.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes, CompilerError } from '@vue/compiler-core' 2 | import { parse, compileTemplate } from '@vue/compiler-sfc' 3 | import { isObject, isString, isSymbol } from '@intlify/shared' 4 | import { traverseI18nCallExpression } from './babel' 5 | 6 | import type { 7 | Node, 8 | TemplateChildNode, 9 | SimpleExpressionNode, 10 | ElementNode, 11 | CompoundExpressionNode, 12 | ExpressionNode, 13 | DirectiveNode, 14 | AttributeNode, 15 | IfNode, 16 | ForNode, 17 | RootNode, 18 | TextCallNode, 19 | InterpolationNode 20 | } from '@vue/compiler-core' 21 | import type { TraverseI18nOptions } from './babel' 22 | 23 | function isRootNode(node: Node): node is RootNode { 24 | return node.type === NodeTypes.ROOT 25 | } 26 | 27 | function isTemplateChildNode(node: Node): node is TemplateChildNode { 28 | return ( 29 | node.type === NodeTypes.ELEMENT || 30 | node.type === NodeTypes.INTERPOLATION || 31 | node.type === NodeTypes.COMPOUND_EXPRESSION || 32 | node.type === NodeTypes.TEXT || 33 | node.type === NodeTypes.COMMENT || 34 | node.type === NodeTypes.IF || 35 | node.type === NodeTypes.IF_BRANCH || 36 | node.type === NodeTypes.FOR || 37 | node.type === NodeTypes.TEXT_CALL 38 | ) 39 | } 40 | 41 | function isElementNode(node: any): node is ElementNode { 42 | return isObject(node) && node.type === NodeTypes.ELEMENT 43 | } 44 | 45 | function isTemplateExpressionNode(node: any): node is ExpressionNode { 46 | return ( 47 | isObject(node) && 48 | (node.type === NodeTypes.SIMPLE_EXPRESSION || 49 | node.type === NodeTypes.COMPOUND_EXPRESSION) 50 | ) 51 | } 52 | 53 | function isInterpolationNode(node: any): node is InterpolationNode { 54 | return isObject(node) && node.type === NodeTypes.INTERPOLATION 55 | } 56 | 57 | function isCompoundExpressionNode(node: any): node is CompoundExpressionNode { 58 | return isObject(node) && node.type === NodeTypes.COMPOUND_EXPRESSION 59 | } 60 | 61 | function isSimpleExpressionNode(node: any): node is SimpleExpressionNode { 62 | return isObject(node) && node.type === NodeTypes.SIMPLE_EXPRESSION 63 | } 64 | 65 | function isIfNode(node: any): node is IfNode { 66 | return isObject(node) && node.type === NodeTypes.IF 67 | } 68 | 69 | function isForNode(node: any): node is ForNode { 70 | return isObject(node) && node.type === NodeTypes.FOR 71 | } 72 | 73 | function isTextCallNode(node: any): node is TextCallNode { 74 | return isObject(node) && node.type === NodeTypes.TEXT_CALL 75 | } 76 | 77 | function isDirectiveNode(node: any): node is DirectiveNode { 78 | return isObject(node) && node.type === NodeTypes.DIRECTIVE 79 | } 80 | 81 | function isAttributeNode(node: any): node is AttributeNode { 82 | return isObject(node) && node.type === NodeTypes.ATTRIBUTE 83 | } 84 | 85 | export interface I18nResourceError extends SyntaxError { 86 | errors: (CompilerError | SyntaxError)[] 87 | } 88 | 89 | type I18nKeyVisitor = (keys: string[]) => void 90 | 91 | /** 92 | * Get i18n resource keys 93 | * 94 | * @param source the source code, which is included i18n resource keys 95 | * @returns the i18n resource keys 96 | */ 97 | export function getResourceKeys( 98 | source: string, 99 | options: TraverseI18nOptions = {} 100 | ): string[] { 101 | let keys: string[] = [] 102 | const visitor = (_keys: string[]): void => { 103 | keys = [...keys, ..._keys] 104 | } 105 | 106 | const { errors, descriptor } = parse(source) 107 | 108 | if (errors.length) { 109 | const error = new Error( 110 | 'Occured at vue compile error. see the `messages` property' 111 | ) as I18nResourceError 112 | error.errors = errors 113 | throw error 114 | } 115 | 116 | if (descriptor.template) { 117 | const templateResult = compileTemplate({ 118 | ...descriptor, 119 | id: 'template.vue' // dummy 120 | }) 121 | 122 | if (templateResult?.ast) { 123 | traverseVueTemplateNode(templateResult.ast, visitor, options) 124 | } 125 | } 126 | 127 | if (descriptor.script || descriptor.scriptSetup) { 128 | const block = descriptor.script || descriptor.scriptSetup 129 | if (block) { 130 | keys = [...keys, ...traverseI18nCallExpression(block.content, options)] 131 | } 132 | } 133 | 134 | return keys 135 | } 136 | 137 | function traverseVueTemplateNode( 138 | node: Node, 139 | visitor: I18nKeyVisitor, 140 | options: TraverseI18nOptions 141 | ): void { 142 | if (isTemplateChildNode(node)) { 143 | if (isTextCallNode(node)) { 144 | traverseVueTemplateNode(node.content, visitor, options) 145 | } else if (isInterpolationNode(node)) { 146 | // console.log('interpolation node', node.type, node.content, node.loc.source) 147 | if (isTemplateExpressionNode(node.content)) { 148 | visitor(traverseI18nCallExpression(node.loc.source, options)) 149 | } 150 | } else if (isCompoundExpressionNode(node)) { 151 | // console.log('compound expression node', node.type, node.loc.source) 152 | node.children.forEach(node => { 153 | if (!isString(node) && !isSymbol(node)) { 154 | traverseVueTemplateNode(node, visitor, options) 155 | } 156 | }) 157 | } else if (isIfNode(node)) { 158 | // console.log('if node', node.type, node, node.loc.source) 159 | node.branches.forEach(node => { 160 | // console.log('if branch node', node) 161 | if (isTemplateExpressionNode(node.condition)) { 162 | visitor( 163 | traverseI18nCallExpression(node.condition.loc.source, options) 164 | ) 165 | } 166 | }) 167 | } else if (isForNode(node)) { 168 | // console.log('fore node', node) 169 | if (isTemplateExpressionNode(node.source)) { 170 | visitor(traverseI18nCallExpression(node.source.loc.source, options)) 171 | } 172 | } else if (isElementNode(node)) { 173 | const elementNode = node 174 | node.props.forEach(node => { 175 | if (isDirectiveNode(node) && isTemplateExpressionNode(node.exp)) { 176 | visitor(traverseI18nCallExpression(node.exp.loc.source, options)) 177 | } else if ( 178 | ['i18n', 'i18n-t'].includes(elementNode.tag) && 179 | isAttributeNode(node) && 180 | node.value 181 | ) { 182 | if ( 183 | (elementNode.tag === 'i18n-t' && node.name === 'keypath') || 184 | (elementNode.tag === 'i18n' && node.name === 'path') 185 | ) { 186 | visitor([node.value.content]) 187 | } 188 | } 189 | }) 190 | node.children.forEach(node => 191 | traverseVueTemplateNode(node, visitor, options) 192 | ) 193 | } 194 | } else if (isSimpleExpressionNode(node)) { 195 | // console.log('simple expression node', node) 196 | visitor(traverseI18nCallExpression(node.loc.source, options)) 197 | } else if (isRootNode(node)) { 198 | node.children.forEach(node => { 199 | traverseVueTemplateNode(node, visitor, options) 200 | }) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /packages/shared/test/babel.test.ts: -------------------------------------------------------------------------------- 1 | import { traverseI18nCallExpression } from '../src/babel' 2 | 3 | test(`t('foo.bar.buz', { name: 'foo' })`, () => { 4 | expect( 5 | traverseI18nCallExpression(`t('foo.bar.buz', { name: 'foo' })`) 6 | ).toEqual(['foo.bar.buz']) 7 | }) 8 | 9 | test("getMessage(`hello! ${$t('foo.bar.buz')}`, param1)", () => { 10 | expect( 11 | traverseI18nCallExpression( 12 | "getMessage(`hello! ${$t('foo.bar.buz')}`, param1)" 13 | ) 14 | ).toEqual(['foo.bar.buz']) 15 | }) 16 | 17 | test(`'foo' + tc('foo.bar', 1);`, () => { 18 | expect(traverseI18nCallExpression(`'foo' + tc('foo.bar');`)).toEqual([ 19 | 'foo.bar' 20 | ]) 21 | }) 22 | 23 | test("`hello! ${$t('foo.bar.buz')}`", () => { 24 | expect(traverseI18nCallExpression("`hello! ${$t('foo.bar.buz')}`")).toEqual([ 25 | 'foo.bar.buz' 26 | ]) 27 | }) 28 | 29 | test('t(`foo.bar.buz`)', () => { 30 | expect(traverseI18nCallExpression('t(`foo.bar.buz`)')).toEqual([ 31 | 'foo.bar.buz' 32 | ]) 33 | }) 34 | 35 | test(`$t(getKey('foo'))`, () => { 36 | expect(traverseI18nCallExpression(`$t(getKey('foo'))`)).toEqual([]) 37 | }) 38 | 39 | test(`({ foo: t('WRRRYYYYYY!!!'), bar: $t('I refuse') })`, () => { 40 | expect( 41 | traverseI18nCallExpression( 42 | `({ foo: t('WRRRYYYYYY!!!'), bar: $t('I refuse') })` 43 | ) 44 | ).toEqual(['WRRRYYYYYY!!!', 'I refuse']) 45 | }) 46 | 47 | test(`this.t('foo.bar.buz')`, () => { 48 | expect(traverseI18nCallExpression(`this.t('foo.bar.buz')`)).toEqual([ 49 | 'foo.bar.buz' 50 | ]) 51 | }) 52 | 53 | test(`global.$t('foo.bar.buz')`, () => { 54 | expect( 55 | traverseI18nCallExpression(`global.$t('foo.bar.buz')`, { 56 | objectIdentifiers: ['global'] 57 | }) 58 | ).toEqual(['foo.bar.buz']) 59 | }) 60 | 61 | test(`window['$t']('foo.bar.buz')`, () => { 62 | expect( 63 | traverseI18nCallExpression(`window['$t']('foo.bar.buz')`, { 64 | objectIdentifiers: ['window'] 65 | }) 66 | ).toEqual(['foo.bar.buz']) 67 | }) 68 | -------------------------------------------------------------------------------- /packages/shared/test/sfc.test.ts: -------------------------------------------------------------------------------- 1 | import { getResourceKeys } from '../src/index' 2 | 3 | describe('getResourceKeys', () => { 4 | test('template interpolation', () => { 5 | const source = `` 9 | expect(getResourceKeys(source)).toEqual(['foo.bar.buz', 'hello']) 10 | }) 11 | 12 | test('javascript expression in template interpolation', () => { 13 | const source = 14 | '' 17 | expect(getResourceKeys(source)).toEqual(['foo.bar.buz']) 18 | }) 19 | 20 | test('html literal + template interpolation', () => { 21 | const source = `` 24 | expect(getResourceKeys(source)).toEqual(['hello']) 25 | }) 26 | 27 | test('directive: v-text', () => { 28 | const source = `` 31 | expect(getResourceKeys(source)).toEqual(['hello DIO!']) 32 | }) 33 | 34 | test('directive: v-bind', () => { 35 | const source = `` 38 | expect(getResourceKeys(source)).toEqual(['Hey Jonathan Joestar!']) 39 | }) 40 | 41 | test('directive: v-on', () => { 42 | const source = `` 45 | expect(getResourceKeys(source)).toEqual(['Qtaro Kujo!']) 46 | }) 47 | 48 | test('directive: v-slot', () => { 49 | const source = `` 54 | expect(getResourceKeys(source)).toEqual(['x', 'y']) 55 | }) 56 | 57 | test('directive: v-show', () => { 58 | const source = `` 61 | expect(getResourceKeys(source)).toEqual(['D4C']) 62 | }) 63 | 64 | test('directive: v-if / v-else-if', () => { 65 | const source = `` 69 | expect(getResourceKeys(source)).toEqual(['bar', 'foo']) 70 | }) 71 | 72 | test('directive: v-for', () => { 73 | const source = `` 78 | }) 79 | 80 | test('custom directive: v-t', () => { 81 | const source = `` 84 | expect(getResourceKeys(source)).toEqual(['The World!']) 85 | }) 86 | 87 | test('include text call nodes', () => { 88 | const source = `` 98 | expect(getResourceKeys(source)).toEqual([ 99 | 'title', 100 | 'pages.home', 101 | 'pages.about' 102 | ]) 103 | }) 104 | 105 | test('script block: Option API', () => { 106 | const source = `` 116 | expect(getResourceKeys(source)).toEqual(['foo.bar.buz']) 117 | }) 118 | 119 | test('script block: Composition API', () => { 120 | const source = `` 131 | expect(getResourceKeys(source)).toEqual(['foo.bar.buz', 'foo']) 132 | }) 133 | 134 | test('script block: Composition API with setup', () => { 135 | const source = `` 142 | expect(getResourceKeys(source)).toEqual([ 143 | 'foo.bar.buz', 144 | 'top level async/await' 145 | ]) 146 | }) 147 | 148 | test('script block: JSX', () => { 149 | // for Option API 150 | const sourceOptionAPI = `` 158 | expect(getResourceKeys(sourceOptionAPI)).toEqual(['hello jsx!']) 159 | 160 | // for Composition API 161 | const source = `` 173 | expect(getResourceKeys(source)).toEqual(['hello JSX!']) 174 | }) 175 | 176 | test('script block: TypeScript', () => { 177 | const source = `` 183 | expect(getResourceKeys(source)).toEqual(['foo.bar.buz']) 184 | }) 185 | 186 | test('i18n component', () => { 187 | const source = `` 192 | expect(getResourceKeys(source)).toEqual(['foo.bar.buz', 'hello']) 193 | }) 194 | 195 | test('i18n-t component', () => { 196 | const source = `` 201 | expect(getResourceKeys(source)).toEqual(['foo.bar.buz', 'hello']) 202 | }) 203 | 204 | test('tempalte & script', () => { 205 | const source = ` 209 | ` 215 | expect(getResourceKeys(source)).toEqual([ 216 | 'foo.bar.buz', 217 | 'hello', 218 | 'foo.bar.buz.foo' 219 | ]) 220 | }) 221 | }) 222 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "commonjs", 6 | "rootDir": "src", 7 | "outDir": "lib" 8 | }, 9 | "include": ["src/**/*"], 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/shell-dev/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 kazuya kawaguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/shell-dev/README.md: -------------------------------------------------------------------------------- 1 | # @intlify-devtools/shell-dev 2 | 3 | ## :copyright: License 4 | 5 | [MIT](http://opensource.org/licenses/MIT) 6 | -------------------------------------------------------------------------------- /packages/shell-dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Intliify Devtools Shell Dev 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/shell-dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@intlify-devtools/shell-dev", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "vue": "^3.0.11", 6 | "vue-i18n": "^9.1.3" 7 | }, 8 | "devDependencies": { 9 | "@intlify-devtools/shared": "0.0.0", 10 | "@intlify/clawler": "0.0.0", 11 | "vite-plugin-intlify-devtools": "0.0.0" 12 | }, 13 | "engines": { 14 | "node": ">= 12" 15 | }, 16 | "private": true, 17 | "scripts": { 18 | "build": "vite build", 19 | "dev": "vite", 20 | "lint": "vuedx-typecheck .", 21 | "serve": "vite preview" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/shell-dev/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 26 | -------------------------------------------------------------------------------- /packages/shell-dev/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intlify/devtools/2b3d932564dcd7c94af3bd02a32ba79a736a7e4c/packages/shell-dev/src/assets/logo.png -------------------------------------------------------------------------------- /packages/shell-dev/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /packages/shell-dev/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp, nextTick } from 'vue' 2 | import App from './App.vue' 3 | import { default as clawl } from '@intlify/clawler' 4 | import WorkerDOM from '@intlify/clawler/dist/worker?worker' 5 | 6 | console.log('shell-dev: App', App) 7 | 8 | const app = createApp(App) 9 | app.mixin({ 10 | mounted() { 11 | if (this._ && this._.type && this._.type.__INTLIFY_META__ && this.$el) { 12 | if (this.$el.nodeType === 3) { 13 | // text node (fragmenet) 14 | this.$el.__INTLIFY_META__ = this._.type.__INTLIFY_META__ 15 | } else { 16 | this.$el.setAttribute('data-intlify', this._.type.__INTLIFY_META__) 17 | this.$el.__INTLIFY_META__ = this._.type.__INTLIFY_META__ 18 | } 19 | } 20 | } 21 | }) 22 | app.mount('#app') 23 | ;(async () => { 24 | await nextTick() 25 | clawl(document.body, WorkerDOM) 26 | })() 27 | -------------------------------------------------------------------------------- /packages/shell-dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "lib": ["esnext", "dom"], 11 | "types": ["vite/client"], 12 | "plugins": [{ "name": "@vuedx/typescript-plugin-vue" }] 13 | }, 14 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/shell-dev/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import vueI18n from '@intlify/vite-plugin-vue-i18n' 4 | import intlifyVue from '@intlify/vite-plugin-vue-i18n/lib/injection' 5 | import intlify from 'vite-plugin-intlify-devtools' 6 | import { generateSecret, encrypt } from '@intlify-devtools/shared' 7 | 8 | // @ts-ignore 9 | const secret = generateSecret() 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | plugins: [ 14 | vue(), 15 | vueI18n(), 16 | intlifyVue({ 17 | __INTLIFY_META__: (a1, a2) => { 18 | const { iv, encryptedData } = encrypt(secret, a1) 19 | return `${iv}$${encryptedData}` 20 | } 21 | }), 22 | intlify({ 23 | devtools: 'http://localhost:3000/devtools' 24 | }) 25 | ], 26 | server: { 27 | proxy: { 28 | '/devtools': { 29 | target: 'http://localhost:5001', 30 | changeOrigin: true, 31 | // rewrite: path => path.replace(/^\/devtools/, '/src/main.ts') 32 | rewrite: path => path.replace(/^\/devtools/, '/devtools.es.js') 33 | } 34 | } 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /packages/vite-plugin-intlify-devtools/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 kazuya kawaguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/vite-plugin-intlify-devtools/README.md: -------------------------------------------------------------------------------- 1 | # vite-plugin-intlify-devtools 2 | 3 | ## :copyright: License 4 | 5 | [MIT](http://opensource.org/licenses/MIT) 6 | -------------------------------------------------------------------------------- /packages/vite-plugin-intlify-devtools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-intlify-devtools", 3 | "description": "vite plugin for intlify devtools", 4 | "version": "0.0.0", 5 | "author": { 6 | "name": "kazuya kawaguchi", 7 | "email": "kawakazu80@gmail.com" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/intlify/devtools/issues" 11 | }, 12 | "dependencies": { 13 | "debug": "^4.3.0" 14 | }, 15 | "devDependencies": { 16 | "@types/debug": "^4.1.5" 17 | }, 18 | "engines": { 19 | "node": ">= 12" 20 | }, 21 | "files": [ 22 | "dist" 23 | ], 24 | "homepage": "https://github.com/intlify/devtools/tree/main/packages/vite-plugin-intlify-devtools#readme", 25 | "license": "MIT", 26 | "main": "lib/index.js", 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/intlify/devtools.git", 30 | "directory": "packages/vite-plugin-intlify-devtools" 31 | }, 32 | "scripts": { 33 | "build": "tsc -b", 34 | "clean": "rm -rf lib/*.* tsconfig.tsbuildinfo", 35 | "watch": "tsc -b --watch" 36 | }, 37 | "types": "lib/index.d.ts" 38 | } 39 | -------------------------------------------------------------------------------- /packages/vite-plugin-intlify-devtools/src/index.ts: -------------------------------------------------------------------------------- 1 | import { debug as Debug } from 'debug' 2 | 3 | import type { Plugin, ResolvedConfig, UserConfig } from 'vite' 4 | 5 | const debug = Debug('vite-plugin-intlify-devtools') 6 | 7 | export type Options = { 8 | devtools?: string 9 | } 10 | 11 | function plugin( 12 | options: Options = { 13 | devtools: 'https://unpkg.com/@intlify/devtools@next' 14 | } 15 | ): Plugin { 16 | debug('plugin options:', options) 17 | 18 | const env = process.env.NODE_ENV || 'development' 19 | 20 | return { 21 | name: 'vite-plugin-intlify-devtools', 22 | 23 | config(config: UserConfig) { 24 | debug('config', config) 25 | }, 26 | 27 | configResolved(_config: ResolvedConfig) { 28 | debug('configResolve', _config) 29 | }, 30 | 31 | async transformIndexHtml(html: string, { path, filename }) { 32 | debug('transformIndexHtml', html, path, filename) 33 | if (env !== 'development') { 34 | return undefined 35 | } 36 | 37 | return { 38 | html, 39 | tags: [ 40 | { 41 | tag: 'script', 42 | attrs: { 43 | type: 'module', 44 | src: options.devtools 45 | }, 46 | injectTo: 'head-prepend' 47 | } 48 | ] 49 | } 50 | }, 51 | 52 | async transform(code: string, id: string) { 53 | // debug('transform', id, code) 54 | return { 55 | code 56 | } 57 | } 58 | } 59 | } 60 | 61 | // overwrite for cjs require('...')() usage 62 | export default plugin 63 | export const intlify = plugin 64 | -------------------------------------------------------------------------------- /packages/vite-plugin-intlify-devtools/test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intlify/devtools/2b3d932564dcd7c94af3bd02a32ba79a736a7e4c/packages/vite-plugin-intlify-devtools/test/.gitkeep -------------------------------------------------------------------------------- /packages/vite-plugin-intlify-devtools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "commonjs", 6 | "rootDir": "src", 7 | "outDir": "lib" 8 | }, 9 | "include": ["src/**/*"], 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", ":preserveSemverRanges"], 3 | "labels": ["Type: Dependency"], 4 | "automerge": true, 5 | "major": { 6 | "automerge": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/checkYarn.js: -------------------------------------------------------------------------------- 1 | if (!/yarn\.js$/.test(process.env.npm_execpath || '')) { 2 | console.warn( 3 | '\u001b[33mThis repository requires Yarn 1.x for scripts to work properly.\u001b[39m\n' 4 | ) 5 | process.exit(1) 6 | } 7 | -------------------------------------------------------------------------------- /scripts/fixpack.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import fixpack from 'fixpack' 3 | import path from 'path' 4 | import { targets } from './utils' 5 | 6 | ;(async () => { 7 | const allTargets = await targets() 8 | const defaultConfig = await import(path.resolve( 9 | __dirname, 10 | '../node_modules/fixpack/config.json' 11 | )) 12 | const { default: rc } = await import(path.resolve(__dirname, '../node_modules/rc')) 13 | const config = rc('fixpack', defaultConfig) 14 | 15 | const allPackages = allTargets.map(target => { 16 | return { 17 | fullPath: path.resolve(__dirname, `../packages/${target}/package.json`), 18 | display: `./packages/${target}/package.json` 19 | } 20 | }) 21 | 22 | // fix packages 23 | allPackages.forEach(({ fullPath, display }) => { 24 | fixpack(fullPath, config) 25 | console.log(chalk.bold(`${display} fixed!`)) 26 | }) 27 | 28 | // fix root 29 | config.quiet = true 30 | delete config.required 31 | fixpack('package.json', config) 32 | console.log(chalk.bold(`./package.json fixed!`)) 33 | })() 34 | -------------------------------------------------------------------------------- /scripts/generateMetaSecret.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import chalk from 'chalk' 3 | import path from 'path' 4 | import { generateSecret } from '@intlify-devtools/shared' 5 | 6 | ;(async () => { 7 | // @ts-ignore 8 | const secret = generateSecret() 9 | await fs.writeFile( 10 | path.resolve(__dirname, '../packages/clawler/.env.local'), 11 | `INTLIFY_META_SECRET=${secret}`, 12 | 'utf-8' 13 | ) 14 | console.log(chalk.bold.green('generate intlify meta secret!')) 15 | })() 16 | -------------------------------------------------------------------------------- /scripts/utils.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import chalk from 'chalk' 3 | 4 | export const targets = async () => { 5 | const packages = await fs.readdir('packages') 6 | return packages.filter(async (f) => { 7 | const stat = await fs.stat(`packages/${f}`) 8 | if (!stat.isDirectory()) { 9 | return false 10 | } 11 | const pkg = await import(`../packages/${f}/package.json`) 12 | // return !pkg.private 13 | return true 14 | }) 15 | } 16 | 17 | export const fuzzyMatchTarget = async (partialTargets: string[], includeAllMatching) => { 18 | const matched: string[] = [] 19 | const _targets = await targets() 20 | partialTargets.forEach(partialTarget => { 21 | for (const target of _targets) { 22 | if (target.match(partialTarget)) { 23 | matched.push(target) 24 | if (!includeAllMatching) { 25 | break 26 | } 27 | } 28 | } 29 | }) 30 | 31 | if (matched.length) { 32 | return matched 33 | } else { 34 | console.log() 35 | console.error( 36 | ` ${chalk.bgRed.white(' ERROR ')} ${chalk.red( 37 | `Target ${chalk.underline(partialTargets)} not found!` 38 | )}` 39 | ) 40 | console.log() 41 | 42 | process.exit(1) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ship.config.js: -------------------------------------------------------------------------------- 1 | const execa = require(require.resolve('execa')) 2 | const { promisify } = require('util') 3 | const fs = require('fs') 4 | const path = require('path') 5 | const read = promisify(fs.readFile) 6 | const write = fs.writeFileSync 7 | 8 | function extractSpecificChangelog(changelog, version) { 9 | if (!changelog) { 10 | return null 11 | } 12 | const escapedVersion = version.replace(/\./g, '\\.') 13 | const regex = new RegExp( 14 | `(#+?\\s\\[?v?${escapedVersion}\\]?[\\s\\S]*?)(#+?\\s\\[?v?\\d\\.\\d\\.\\d\\]?)`, 15 | 'g' 16 | ) 17 | const matches = regex.exec(changelog) 18 | return matches ? matches[1] : null 19 | } 20 | 21 | async function commitChangelog(current, next) { 22 | const { stdout } = await execa('npx', [ 23 | 'lerna-changelog', 24 | '--next-version', 25 | `v${next}` 26 | ]) 27 | const escapedVersion = next.replace(/\./g, '\\.') 28 | const regex = new RegExp( 29 | `(#+?\\s\\[?v?${escapedVersion}\\]?[\\s\\S]*?)(#+?\\s\\[?v?\\d\\.\\d\\.\\d\\]?)`, 30 | 'g' 31 | ) 32 | const matches = regex.exec(stdout.toString()) 33 | const head = matches ? matches[1] : stdout 34 | const changelog = await read('./CHANGELOG.md', 'utf8') 35 | return write('./CHANGELOG.md', `${head}\n\n${changelog}`) 36 | } 37 | 38 | module.exports = { 39 | mergeStrategy: { toSameBranch: ['master'] }, 40 | monorepo: undefined, 41 | updateChangelog: false, 42 | beforeCommitChanges: ({ nextVersion, exec, dir }) => { 43 | return new Promise(resolve => { 44 | const pkg = require('./package.json') 45 | commitChangelog(pkg.version, nextVersion).then(resolve) 46 | }) 47 | }, 48 | formatCommitMessage: ({ version, releaseType, mergeStrategy, baseBranch }) => 49 | `${releaseType} release v${version}`, 50 | formatPullRequestTitle: ({ version, releaseType }) => 51 | `${releaseType} release v${version}`, 52 | shouldRelease: () => true, 53 | releases: { 54 | extractChangelog: ({ version, dir }) => { 55 | const changelogPath = path.resolve(dir, 'CHANGELOG.md') 56 | try { 57 | const changelogFile = fs.readFileSync(changelogPath, 'utf-8').toString() 58 | const ret = extractSpecificChangelog(changelogFile, version) 59 | return ret 60 | } catch (err) { 61 | if (err.code === 'ENOENT') { 62 | return null 63 | } 64 | throw err 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "dist" /* Redirect output structure to the directory. */, 16 | // "rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | "composite": true /* Enable project compilation */, 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": false, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 43 | "baseUrl": "." /* Base directory to resolve non-absolute module names. */, 44 | "paths": { 45 | "@intlify-devtools/*": ["packages/*/src"] 46 | // "@intlify/vite-plugin-vue-i18n": ["./node_modules/@intlify/vite-plugin-vue-i18n"] 47 | } /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */, 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | }, 69 | "include": ["packages/*/src", "packages/*/test"], 70 | "exclude": ["node_modules"], 71 | "plugins": [ 72 | { 73 | "name": "typescript-eslint-language-service" 74 | } 75 | ] 76 | } 77 | --------------------------------------------------------------------------------