diff --git a/.github/workflows/auto-translate.yml b/.github/workflows/auto-translate.yml new file mode 100644 index 000000000..d27ad5b76 --- /dev/null +++ b/.github/workflows/auto-translate.yml @@ -0,0 +1,62 @@ +name: Auto-Translate Questions + +on: + push: + paths: + - 'src/questions/**/*.md' + branches: [ main ] + workflow_dispatch: + inputs: + language: + description: 'Specific language to translate (leave empty for all)' + required: false + default: '' + +jobs: + translate: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run translations + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + if [ -n "${{ github.event.inputs.language }}" ]; then + echo "Translating specific language: ${{ github.event.inputs.language }}" + npm run translate "${{ github.event.inputs.language }}" + else + echo "Translating all configured languages" + npm run translate:all + fi + + - name: Check for changes + id: verify-changed-files + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "changed=true" >> $GITHUB_OUTPUT + else + echo "changed=false" >> $GITHUB_OUTPUT + fi + + - name: Commit and push changes + if: steps.verify-changed-files.outputs.changed == 'true' + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add src/translations/ + git commit -m "Auto-update translations [skip ci]" + git push \ No newline at end of file diff --git a/README.md b/README.md index e9e54b4dc..93809f607 100755 --- a/README.md +++ b/README.md @@ -25,6 +25,25 @@ You can read more about this project & its history [here](https://h5bp.org/Front 2. [How to Contribute](https://github.com/h5bp/Front-end-Developer-Interview-Questions/blob/master/.github/CONTRIBUTING.md) 3. [License](https://github.com/h5bp/Front-end-Developer-Interview-Questions/blob/master/LICENSE.md) +## Automated Translations + +This repository now features automated translation capabilities! 🌍 + +When English questions are updated, translations are automatically generated using AI to keep all language versions in sync. This ensures that our international community always has access to the latest questions. + +### How it works: +- English questions are maintained in separate files in [`src/questions/`](src/questions/) +- Automated translation system converts them into combined language-specific files +- Translations are generated using AI with technical accuracy and professional tone +- Available in 30+ languages and growing + +### For Maintainers: +- Translations update automatically when questions change +- Manual translation workflow available: `npm run translate [language]` +- Full documentation in [`scripts/README.md`](scripts/README.md) + +See our [translation documentation](scripts/README.md) for more details. + The project is currently maintained by: diff --git a/package-lock.json b/package-lock.json index 0718b4645..b5eb97586 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "luxon": "^3.4.4", "markdown-it": "^14.1.0", "markdown-it-anchor": "^8.6.7", + "openai": "^4.20.1", "uglify-es": "^3.3.9" } }, @@ -359,12 +360,46 @@ "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", "dev": true }, + "node_modules/@types/node": { + "version": "18.19.123", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.123.tgz", + "integrity": "sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, "node_modules/a-sync-waterfall": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", "dev": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -377,6 +412,19 @@ "node": ">=0.4.0" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/all-contributors-cli": { "version": "6.26.1", "resolved": "https://registry.npmjs.org/all-contributors-cli/-/all-contributors-cli-6.26.1.tgz", @@ -537,6 +585,13 @@ "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/babel-walk": { "version": "3.0.0-canary-5", "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", @@ -638,6 +693,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/camel-case": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", @@ -778,6 +847,19 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", @@ -840,6 +922,16 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dependency-graph": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", @@ -937,6 +1029,21 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -997,6 +1104,55 @@ "errno": "cli.js" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1025,6 +1181,16 @@ "node": ">=4" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", @@ -1166,6 +1332,44 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1187,10 +1391,14 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-caller-file": { "version": "2.0.5", @@ -1202,19 +1410,44 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -1247,6 +1480,19 @@ "node": ">= 6" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1317,10 +1563,27 @@ } }, "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, "engines": { "node": ">= 0.4" }, @@ -1328,6 +1591,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1398,6 +1674,16 @@ "node": ">= 0.10" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1837,6 +2123,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/maximatch": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/maximatch/-/maximatch-0.1.0.tgz", @@ -1922,6 +2218,29 @@ "node": ">=10.0.0" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -2031,6 +2350,27 @@ "lower-case": "^1.1.1" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -2139,6 +2479,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -2945,6 +3316,13 @@ "node": ">=0.8.0" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2969,6 +3347,16 @@ "node": ">=0.10.0" } }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -3034,10 +3422,11 @@ "dev": true }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 9e7326018..f1ca694d2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "build": "eleventy --config=config/eleventy.config.js --pathprefix='Front-end-Developer-Interview-Questions/'", "contributors:add": "all-contributors add", "contributors:generate": "all-contributors generate && cp .all-contributorsrc ./src/_data/contributors.json", - "start": "eleventy --config=config/eleventy.config.js --serve --port 9090 --quiet" + "start": "eleventy --config=config/eleventy.config.js --serve --port 9090 --quiet", + "translate": "node scripts/translate.js", + "translate:all": "node scripts/translate.js all" }, "repository": { "type": "git", @@ -45,6 +47,7 @@ "luxon": "^3.4.4", "markdown-it": "^14.1.0", "markdown-it-anchor": "^8.6.7", + "openai": "^4.20.1", "uglify-es": "^3.3.9" }, "resolutions": { diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 000000000..5d7abc4fc --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,129 @@ +# Translation Automation System + +This directory contains the automated translation system for the Front-end Developer Interview Questions repository. + +## Overview + +The translation system automatically converts English questions from `src/questions/` into translated versions for each target language in `src/translations/`. + +## Files + +- `translate.js` - Main translation script +- `translation-config.json` - Configuration for languages and AI settings + +## Usage + +### Prerequisites + +1. Set up OpenAI API key (optional - falls back to mock translations if not available): + ```bash + export OPENAI_API_KEY="your-api-key-here" + ``` + +### Running Translations + +#### Translate all languages: +```bash +npm run translate:all +``` + +#### Translate a specific language: +```bash +npm run translate spanish +# or +node scripts/translate.js spanish +``` + +#### Available languages: +- `test-lang` - Test language for development +- `spanish` - Spanish translation +- `french` - French translation +- `chinese` - Chinese translation + +### Adding New Languages + +1. Add language configuration to `translation-config.json`: +```json +{ + "languages": { + "your-language": { + "title": "Your Language Title", + "lang": "language-code", + "rtl": false, + "sections": { + "general": "General Questions Translation", + "html": "HTML Questions Translation", + // ... etc + } + } + } +} +``` + +2. Create the target directory: +```bash +mkdir -p src/translations/your-language +``` + +3. Run the translation: +```bash +npm run translate your-language +``` + +## How It Works + +1. **Question Extraction**: Reads all English question files from `src/questions/` +2. **AI Translation**: Uses OpenAI GPT-4 to translate questions while preserving format +3. **File Generation**: Creates translated README.md files using the template structure +4. **Format Preservation**: Maintains markdown formatting, code snippets, and navigation + +## Question Formats Supported + +- **Bullet Point Questions**: Standard `* Question text` format +- **Coding Questions**: `Question: What is...` format with code blocks +- **Nested Questions**: Indented sub-questions with ` * Sub-question` + +## Configuration + +### Language Configuration +Each language needs: +- `title`: Translated page title +- `lang`: Language code (e.g., 'es', 'fr', 'zh') +- `rtl`: Boolean for right-to-left languages +- `sections`: Translated section headers + +### AI Configuration +- `provider`: Currently supports "openai" +- `model`: AI model to use (default: "gpt-4") +- `systemPrompt`: Instructions for the AI translator + +## Development + +### Testing +Use the `test-lang` language for development and testing: +```bash +npm run translate test-lang +``` + +### Mock Translations +When OpenAI API is not available, the system falls back to mock translations that prefix questions with `[LANGUAGE]` for testing. + +## Integration with Build Process + +The translation system can be integrated into CI/CD workflows to automatically update translations when English questions change. + +### GitHub Actions Example: +```yaml +- name: Update Translations + run: | + export OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} + npm run translate:all +``` + +## Future Enhancements + +- Support for additional AI providers (Azure OpenAI, Google Translate, etc.) +- Incremental translations (only update changed questions) +- Translation quality validation +- Support for more complex markdown structures +- Translation memory and consistency checking \ No newline at end of file diff --git a/scripts/translate.js b/scripts/translate.js new file mode 100644 index 000000000..228211771 --- /dev/null +++ b/scripts/translate.js @@ -0,0 +1,272 @@ +#!/usr/bin/env node + +/** + * Translation automation script for Front-end Developer Interview Questions + * + * This script reads English question files from src/questions/ and generates + * translated versions for each target language in src/translations/ + */ + +const fs = require('fs').promises; +const path = require('path'); +const config = require('./translation-config.json'); +const OpenAI = require('openai'); + +class QuestionTranslator { + constructor() { + this.sourceDir = path.join(__dirname, '../src/questions'); + this.translationsDir = path.join(__dirname, '../src/translations'); + this.templatePath = path.join(this.translationsDir, '_template/README.md'); + + // Initialize OpenAI client if API key is available + const apiKey = process.env.OPENAI_API_KEY || config.ai.apiKey.replace('${OPENAI_API_KEY}', ''); + if (apiKey && apiKey !== '${OPENAI_API_KEY}') { + this.openai = new OpenAI({ apiKey }); + } + } + + /** + * Read all English question files + */ + async readSourceQuestions() { + const questionFiles = await fs.readdir(this.sourceDir); + const questions = {}; + + for (const file of questionFiles) { + if (file.endsWith('.md')) { + const content = await fs.readFile(path.join(this.sourceDir, file), 'utf-8'); + const category = file.replace('.md', '').replace('-questions', ''); + questions[category] = this.extractQuestions(content); + console.log(`Extracted ${questions[category].length} questions from ${category}`); + } + } + + return questions; + } + + /** + * Extract questions from markdown content + */ + extractQuestions(content) { + const lines = content.split('\n'); + const questions = []; + let pastFrontMatter = false; + let currentQuestion = ''; + let inCodeBlock = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Skip front matter + if (line.trim() === '---') { + if (!pastFrontMatter) { + pastFrontMatter = true; + // Skip until we find the closing --- + while (i + 1 < lines.length && lines[i + 1].trim() !== '---') { + i++; + } + i++; // Skip the closing --- + continue; + } + } + + if (!pastFrontMatter) continue; + + // Handle code blocks + if (line.trim().startsWith('```')) { + inCodeBlock = !inCodeBlock; + } + + // Collect question lines (bullet points) + if (line.trim().startsWith('* ') || line.trim().startsWith(' * ')) { + questions.push(line); + } + // Handle "Question:" format for coding questions + else if (line.trim().startsWith('Question:')) { + if (currentQuestion) { + questions.push(`* ${currentQuestion.trim()}`); + } + currentQuestion = line.trim().replace('Question:', '').trim(); + } + // Continue multi-line questions or code blocks + else if (currentQuestion && (line.trim() || inCodeBlock)) { + currentQuestion += '\n' + line; + } + } + + // Add the last question if exists + if (currentQuestion) { + questions.push(`* ${currentQuestion.trim()}`); + } + + return questions; + } + + /** + * Translate questions using AI + */ + async translateQuestions(questions, targetLanguage) { + console.log(`Translating questions to ${targetLanguage}...`); + + const languageConfig = config.languages[targetLanguage]; + if (!languageConfig) { + throw new Error(`Language ${targetLanguage} not found in configuration`); + } + + const translated = {}; + + // If OpenAI is not available, use mock translations + if (!this.openai) { + console.log('OpenAI API not available, using mock translations'); + for (const [category, questionList] of Object.entries(questions)) { + translated[category] = questionList.map(q => `[${targetLanguage.toUpperCase()}] ${q}`); + } + return translated; + } + + // Use AI to translate each category + for (const [category, questionList] of Object.entries(questions)) { + if (questionList.length === 0) continue; + + try { + console.log(` Translating ${category} questions...`); + const questionsText = questionList.join('\n'); + + const response = await this.openai.chat.completions.create({ + model: config.ai.model, + messages: [ + { + role: 'system', + content: `${config.ai.systemPrompt} + +Target language: ${languageConfig.lang} (${targetLanguage}) + +Important: +- Maintain the exact markdown bullet point format (* and *) +- Preserve all code snippets and technical terms +- Keep the professional interview tone +- Do not add explanations or extra text` + }, + { + role: 'user', + content: questionsText + } + ], + temperature: 0.3, + max_tokens: 4000 + }); + + const translatedText = response.choices[0].message.content.trim(); + translated[category] = translatedText.split('\n').filter(line => line.trim()); + + } catch (error) { + console.error(`Error translating ${category}:`, error.message); + // Fallback to mock translation + translated[category] = questionList.map(q => `[${targetLanguage.toUpperCase()}] ${q}`); + } + } + + return translated; + } + + /** + * Generate translated README.md file + */ + async generateTranslationFile(translatedQuestions, language) { + const languageConfig = config.languages[language]; + if (!languageConfig) { + throw new Error(`Language ${language} not found in configuration`); + } + + const template = await this.loadTemplate(); + let content = template; + + // Update front matter with language-specific information + content = content.replace( + /---\ntitle: .*?\nlayout: .*?\npermalink: .*?\n---/, + `---\ntitle: ${languageConfig.title}\nlayout: layouts/page.njk\npermalink: /translations/${language}/index.html\nlang: ${languageConfig.lang}\n---` + ); + + // Replace each section's questions individually + const sections = ['general', 'html', 'css', 'javascript', 'coding', 'testing', 'performance', 'network', 'fun']; + + for (const section of sections) { + if (translatedQuestions[section]) { + // Find the section header and replace the content that follows + const sectionHeaderPattern = new RegExp( + `(#### \\[\\[⬆\\]\\]\\(#toc\\) )[^<]*(\\s*\\n\\n)([\\s\\S]*?)(?=\\n\\n#### |$)`, + 'g' + ); + + const sectionTitle = languageConfig.sections[section] || `${section} Questions`; + const questionText = translatedQuestions[section].join('\n'); + + content = content.replace(sectionHeaderPattern, + `$1${sectionTitle}:$2${questionText}\n\n` + ); + } + } + + return content; + } + + /** + * Load template file + */ + async loadTemplate() { + return await fs.readFile(this.templatePath, 'utf-8'); + } + + /** + * Main translation function + */ + async translateToLanguage(language) { + console.log(`Starting translation for ${language}...`); + + const questions = await this.readSourceQuestions(); + const translatedQuestions = await this.translateQuestions(questions, language); + const content = await this.generateTranslationFile(translatedQuestions, language); + + const outputPath = path.join(this.translationsDir, language, 'README.md'); + await fs.writeFile(outputPath, content, 'utf-8'); + + console.log(`Translation completed for ${language}: ${outputPath}`); + } + + /** + * Translate all configured languages + */ + async translateAll() { + for (const language of Object.keys(config.languages)) { + try { + await this.translateToLanguage(language); + } catch (error) { + console.error(`Error translating ${language}:`, error.message); + } + } + } +} + +// CLI interface +async function main() { + const translator = new QuestionTranslator(); + const args = process.argv.slice(2); + + if (args.length === 0) { + console.log('Usage: node translate.js [language|all]'); + console.log('Available languages:', Object.keys(config.languages).join(', ')); + return; + } + + if (args[0] === 'all') { + await translator.translateAll(); + } else { + await translator.translateToLanguage(args[0]); + } +} + +if (require.main === module) { + main().catch(console.error); +} + +module.exports = QuestionTranslator; \ No newline at end of file diff --git a/scripts/translation-config.json b/scripts/translation-config.json new file mode 100644 index 000000000..5b0b729f1 --- /dev/null +++ b/scripts/translation-config.json @@ -0,0 +1,90 @@ +{ + "languages": { + "spanish": { + "title": "GUÍA DE PREGUNTAS PARA ENTREVISTAS DE TRABAJO", + "lang": "es", + "rtl": false, + "sections": { + "general": "Preguntas generales", + "html": "Preguntas específicas de HTML", + "css": "Preguntas específicas de CSS", + "javascript": "Preguntas específicas de JavaScript", + "coding": "Preguntas de código", + "testing": "Preguntas sobre pruebas de código", + "performance": "Preguntas sobre rendimiento", + "network": "Preguntas sobre conectividad", + "fun": "Preguntas divertidas" + } + }, + "french": { + "title": "Questionnaire de recrutement pour développeur front-end", + "lang": "fr", + "rtl": false, + "sections": { + "general": "Questions générales", + "html": "Questions sur HTML", + "css": "Questions sur CSS", + "javascript": "Questions sur JS", + "coding": "Questions sur la programmation", + "testing": "Questions sur les tests", + "performance": "Questions sur la performance", + "network": "Questions sur réseau", + "fun": "Questions pour le fun" + } + }, + "chinese": { + "title": "前端工作面试问题", + "lang": "zh", + "rtl": false, + "sections": { + "general": "常见问题", + "html": "HTML 相关问题", + "css": "CSS 相关问题", + "javascript": "JS 相关问题", + "coding": "代码相关问题", + "testing": "测试相关问题", + "performance": "效能相关问题", + "network": "网络相关问题", + "fun": "趣味问题" + } + }, + "german": { + "title": "Frontend-Entwickler Interviewfragen", + "lang": "de", + "rtl": false, + "sections": { + "general": "Allgemeine Fragen", + "html": "HTML-Fragen", + "css": "CSS-Fragen", + "javascript": "JavaScript-Fragen", + "coding": "Code-Fragen", + "testing": "Test-Fragen", + "performance": "Performance-Fragen", + "network": "Netzwerk-Fragen", + "fun": "Spaß-Fragen" + } + }, + "italian": { + "title": "Domande per il colloquio di lavoro per sviluppatori front-end", + "lang": "it", + "rtl": false, + "sections": { + "general": "Domande generali", + "html": "Domande HTML", + "css": "Domande CSS", + "javascript": "Domande JavaScript", + "coding": "Domande di codifica", + "testing": "Domande sui test", + "performance": "Domande sulle prestazioni", + "network": "Domande di rete", + "fun": "Domande divertenti" + } + } + }, + "ai": { + "provider": "openai", + "model": "gpt-4", + "apiKey": "${OPENAI_API_KEY}", + "systemPrompt": "You are a professional translator specializing in technical documentation. Translate the following front-end developer interview questions accurately while maintaining their technical meaning and professional tone. Preserve any code snippets, technical terms, and markdown formatting exactly as they appear." + } +} \ No newline at end of file