diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ac44141 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing to Tranzlate + +Thank you for your interest in contributing to Tranzlate! Whether you want to report a bug, propose a new feature, or contribute code, we appreciate your help in making this tool better. + +## How Can I Contribute? + +### Reporting Bugs + +If you encounter a bug while using Tranzlate, please check if it hasn't been reported already by searching through our [issues](https://github.com/wkaisertexas/tranzlate/issues). If it's a new issue, you can create a detailed bug report including: + +- Description of the issue +- Steps to reproduce +- Expected behavior +- Actual behavior +- Environment details (Node.js version, operating system, etc.) + +### Requesting Features + +You can request new features or improvements by creating a [feature request](https://github.com/wkaisertexas/tranzlate/issues/new?assignees=&labels=feature-request&template=feature_request.md&title=) issue. Provide a clear description of the feature and why it would be valuable. + +### Contributing Code + +We welcome contributions in the form of pull requests (PRs). To contribute code: + +1. Fork the repository and create your branch from `main`. +2. Implement your changes, following the [coding style](#coding-style) and ensuring tests pass. +3. Open a pull request with a clear description of your changes and the problem they solve. + +#### Coding Style + +- Follow existing code style and formatting conventions. +- Document new code or changes using JSDoc comments where applicable. +- Write clear commit messages explaining the purpose of each commit. + +## Setting Up Development Environment + +To set up the Tranzlate development environment: + +1. Clone the repository: + + ```bash + git clone https://github.com/wkaisertexas/tranzlate + ``` + +2. Install dependencies: + + ```bash + npm install + ``` + +3. Set up your OpenAI API key as described in the [Setup](#setup) section of the documentation. + +4. You're ready to start coding! Make your changes and test them locally. + +## Code Review Process + +All code contributions will go through a code review process. Be prepared to address feedback and iterate on your changes before they can be merged. + +## License + +By contributing to Tranzlate, you agree that your contributions will be licensed under the [MIT License](LICENSE). diff --git a/package-lock.json b/package-lock.json index 04146ec..bb4c139 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,7 @@ "ini": "^4.1.1", "openai": "^4.24.1", "picocolors": "^1.0.0", - "pkgroll": "^2.0.1", - "tranzlate.js": "^1.0.0" + "pkgroll": "^2.0.1" }, "bin": { "translate": "dist/index.js", @@ -27,6 +26,7 @@ "devDependencies": { "husky": "^4.3.8", "lint-staged": "^15.2.0", + "nodemon": "^3.1.4", "prettier": "3.1.1" } }, @@ -241,7 +241,6 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -1089,6 +1088,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1146,6 +1159,19 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/blessed": { "version": "0.1.81", "resolved": "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz", @@ -1237,6 +1263,44 @@ "node": "*" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -2233,6 +2297,13 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2284,6 +2355,19 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -2981,6 +3065,68 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/nodemon": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", + "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-path": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", @@ -3317,6 +3463,13 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3352,6 +3505,19 @@ } ] }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -3575,6 +3741,19 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/semver-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", @@ -3623,6 +3802,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -3859,6 +4051,16 @@ "node": ">=8.0" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -3876,21 +4078,6 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "node_modules/tranzlate.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tranzlate.js/-/tranzlate.js-1.0.0.tgz", - "integrity": "sha512-XHAvrhAdpIsYjn+Oomm1B6zB1H4iuIscP/VXHzyIq9SXXVIAz0gnSRJ83VBXgEyv9efItG1a3ECtXhG0x+WxbQ==", - "dependencies": { - "@clack/prompts": "^0.7.0", - "clack": "^0.1.0", - "cleye": "^1.3.2", - "openai": "^4.24.1", - "picocolors": "^1.0.0" - }, - "bin": { - "tranzlate": "dist/index.js" - } - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -3950,6 +4137,13 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/package.json b/package.json index e52fc70..0418f8c 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "devDependencies": { "husky": "^4.3.8", "lint-staged": "^15.2.0", + "nodemon": "^3.1.4", "prettier": "3.1.1" } } diff --git a/src/consts.js b/src/consts.js index 971bb0f..9675b3f 100644 --- a/src/consts.js +++ b/src/consts.js @@ -1,5 +1,5 @@ const DEFAULT_INPUT_FILE = "Localizable.xcstrings"; -const VALID_MODELS = ["gpt-3.5-turbo", "gpt-4"]; +const VALID_MODELS = ["gpt-4o", "gpt-4", "gpt-3.5-turbo"]; const SUPPORTED_TRANSLATIONS = [ "en", "zh-Hans", diff --git a/src/translate.js b/src/translate.js index 35ce610..d5e2ab7 100644 --- a/src/translate.js +++ b/src/translate.js @@ -4,25 +4,6 @@ import { readFileSync, writeFileSync } from "fs"; import { LANGUAGES, BASE_REVIEW_STATE } from "./consts.js"; -// MAKE prompt should be made internationale, but this needs to happen later -const makePrompt = ({ - projectDescription, - comment, - string, - sourceLanguage, - targetLanguage, -}) => ` -${projectDescription} - -${comment ? `a translation comment has been made: ${comment}\n` : ""} -input string which should be translated to ${LANGUAGES[targetLanguage]}: - -${string} - -Translation from ${LANGUAGES[sourceLanguage]} to ${ - LANGUAGES[targetLanguage] -}: `; - const readStringCatalog = (path) => { let stringData = readFileSync(path); return JSON.parse(stringData); @@ -51,21 +32,20 @@ const multiTranslate = async ({ }) => { let returnValue = {}; // return value has `state` and `value` keys - await Promise.all( - languages.map(async (language) => { - let completion = await getCompletion({ - string: key, - comment: strings[key].comment, - targetLanguage: language, - sourceLanguage, - description, - }); - returnValue[language] = { - value: completion, - state: review_state, - }; - }), - ); + let completion = await getCompletion({ + string: key, + comment: strings[key].comment, + targetLanguages: languages, + sourceLanguage, + description, + }); + + Object.keys(completion).forEach((key) => { + returnValue[key] = { + value: completion[key], // the translated string + state: review_state, + } + }); return { localizations: returnValue }; }; @@ -74,33 +54,31 @@ const getCompletion = async ({ string, comment, sourceLanguage, - targetLanguage, + targetLanguages, description, }) => { - const prompt = makePrompt({ - projectDescription: description, - string, - comment, - sourceLanguage, - targetLanguage, - }); + let commentString = comment ? `A comment from the developer: ${comment}` : ""; + let descriptionPrompt = description ? `The project description is: ${description}` : ""; + const gptResponse = await openai.chat.completions.create({ model: completionModel, messages: [ { role: "system", - content: prompt, - }, + content: `You are a translation expert from ${sourceLanguage} to the following languages: ${targetLanguages.map((lang) => LANGUAGES[lang] + "(" + lang + ")").join(", ")}. ${descriptionPrompt} You are asked to translate strings and return the result in a json object with the language code as the key and the translation as the value. Do not modify template strings (e.g. %lld) in any way. ${commentString}`, + }, { role: "user", content: string, }, ], + response_format: {type: "json_object"}, // should give us a json response }); - // TODO: introduce some error handling here - let response = gptResponse.choices[0].message.content; + + response = JSON.parse(response); // turn response into a json object (should work) + return response; }; @@ -124,7 +102,7 @@ const translate = async ({ apiKey: apiKey, }); completionModel = model; - review_state = state; + review_state = state ? state : BASE_REVIEW_STATE; let newStrings = await Promise.all( Object.keys(strings).map( @@ -154,4 +132,4 @@ const translate = async ({ writeStringCatalog(outputFile, newStringCatalog); }; -export { translate, writeStringCatalog }; +export { translate, writeStringCatalog }; \ No newline at end of file