Add automated translation system for multilingual question maintenance

Co-authored-by: roblarsen <361421+roblarsen@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-08-26 02:57:51 +00:00
parent daa5e6e62e
commit ebcaa7f017
7 changed files with 981 additions and 17 deletions

62
.github/workflows/auto-translate.yml vendored Normal file
View File

@ -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

View File

@ -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:

421
package-lock.json generated
View File

@ -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,11 @@
}
},
"node_modules/has-symbols": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
"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"
},
@ -1328,6 +1575,35 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"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"
},
"funding": {
"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"
},

View File

@ -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": {

129
scripts/README.md Normal file
View File

@ -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

272
scripts/translate.js Normal file
View File

@ -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\\) <a name='${section}'>)[^<]*(</a>\\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;

View File

@ -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."
}
}