@drone1/alt 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,15 +13,46 @@
13
13
  ![Contributions Welcome](https://img.shields.io/badge/contributions-welcome-brightgreen)
14
14
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
15
15
 
16
+ <!--ts-->
17
+ * [AI Localization Tool](#ai-localization-tool)
18
+ * [Features](#features)
19
+ * [Installation](#installation)
20
+ * [Setup](#setup)
21
+ * [Create a reference file](#create-a-reference-file)
22
+ * [Running](#running)
23
+ * [Output](#output)
24
+ * [Config file](#config-file)
25
+ * [Adding context](#adding-context)
26
+ * [Application-level context](#application-level-context)
27
+ * [String-specific context](#string-specific-context)
28
+ * [Display language](#display-language)
29
+ * [Usage](#usage)
30
+ * [Examples](#examples)
31
+ * [Example I](#example-i)
32
+ * [Example II](#example-ii)
33
+ * [Example III](#example-iii)
34
+ * [Example: ALT's localized display strings](#example-alts-localized-display-strings)
35
+ * [Formatting](#formatting)
36
+ * [Translation rules](#translation-rules)
37
+ * [Additional notes](#additional-notes)
38
+ * [Delayed vs. realtime writes](#delayed-vs-realtime-writes)
39
+ * [CI](#ci)
40
+ * [PR](#pr)
41
+
42
+ <!-- Created by https://github.com/ekalinin/github-markdown-toc -->
43
+ <!-- Added by: jonl, at: Wed Apr 16 11:56:33 AM CEST 2025 -->
44
+
45
+ <!--te-->
46
+
16
47
  # AI Localization Tool
17
- Translates all source strings in a reference (`.js`,`.mjs`,`.json`,`.jsonc`) file to all target languages using AI.
48
+ Translates all source strings in a reference (`js`,`mjs`,`json`,`jsonc`) file to all target languages using AI.
18
49
 
19
50
  ## Features
20
51
  * Loads source/reference key/value pairs from a file
21
52
  * Localizes using AI as needed, writing to a .json file per language
22
53
  * App-level context can be specified [`appContextMessage`]
23
54
  * Additional context can be specified per string [`--contextPrefix`, `--contextSuffix`]
24
- * Supports multiple AI providers: Claude, OpenAI [`--provider`]
55
+ * Supports Claude, Gemini, OpenAI [`--provider`]
25
56
  * User-modifications to output files are safe and will not be overwritten
26
57
  * Languages are specified using BCP47 tags
27
58
 
@@ -30,7 +61,10 @@ Translates all source strings in a reference (`.js`,`.mjs`,`.json`,`.jsonc`) fil
30
61
  npm install -g @drone1/alt
31
62
  ```
32
63
  ## Setup
33
- Create a reference file for your reference data. For example, ``reference.js``:
64
+ ### Create a reference file
65
+ This will house your source strings via key/value pairs in your preferred language.
66
+
67
+ Here's an example ``reference.js``:
34
68
  ```javascript
35
69
  export default {
36
70
  'error-msg': `Sorry, we don't know how to do anything`,
@@ -38,22 +72,27 @@ export default {
38
72
  '_context:success-msg': `This text is for a button when a user completes a task`
39
73
  }
40
74
  ```
41
- Use whatever filename you prefer, but currently a .js extension is required.
42
- You can specify an exported variable instead of using `default`. See `--referenceVarName`.
75
+ Use whatever filename you prefer. `js`,`mjs`,`json`,`jsonc` extensions are supported.
43
76
 
44
- 2. Running
77
+ For `.js` and `.mjs` files, you can specify the name of an exported variable instead of using `default`, via `--referenceVarName`.
78
+
79
+ ### Running
45
80
  ```bash
46
81
  ANTHROPIC_API_KEY=<secret>
47
- alt translate --reference-file ./reference.js --reference-language en --target-languages aa,bo,es-MX,hi,zh-SG --provider anthropic
48
- ```
49
- or
50
- ```bash
51
- OPENAI_API_KEY=<secret>
52
- alt translate --reference-file ./reference.js --reference-language en --target-languages aa,bo,es-MX,hi,zh-SG --provider openai
82
+ alt translate --reference-file ./reference.js --reference-language en --target-languages aa,bo,es-MX,hi,zh-Hans --provider anthropic
53
83
  ```
54
- These commands would iterate across all key/value pairs in the variable exported from `./reference.js` and if needed, translate.
84
+ This command would iterate across all key/value pairs defined in `./reference.js` and translate if needed.
85
+
86
+ Here are all supported providers and their required environment variable
87
+
88
+ | `-p`, `--provider` | <span style="font-weight: normal;">environment variable</span> |
89
+ |-------------------|----------------------------------------------------------------|
90
+ | anthropic | ANTHROPIC_API_KEY |
91
+ | google | GOOGLE_API_KEY |
92
+ | openai | OPENAI_API_KEY |
55
93
 
56
- The examples above would write `aa.json`, `bo.json`, etc., to the current working directory.
94
+ ### Output
95
+ The example above would write `aa.json`, `bo.json`, etc., to the current working directory.
57
96
 
58
97
  Sample output:
59
98
  ```json
@@ -65,10 +104,6 @@ Sample output:
65
104
 
66
105
  Note that output files can be lower-cased if you pass the ``--normalize-output-filenames`` option, so `fr-FR` translations would write to `fr-fr.json`
67
106
 
68
- ## Display language
69
- ALT CLI itself has been localized so you can use it many languages. For non-English languages, you can set the display language with the `ALT_LANGUAGE` environment variable. Please feel free to submit
70
- an issue if you do not see your preferred language.
71
-
72
107
  ## Config file
73
108
  [_optional_] You can create a config file. By default, `ALT` will search the output directory for `config.json`, but you can specify a path directly using
74
109
  `--config-file`.
@@ -79,7 +114,7 @@ config:
79
114
  {
80
115
  "appContextMessage": "This is a description of my app",
81
116
  "referenceLanguage": "ar",
82
- "provider": "anthropic",
117
+ "provider": "google",
83
118
  "lookForContextData": true,
84
119
  "contextPrefix": "_context:",
85
120
  "contextSuffix": "",
@@ -91,6 +126,40 @@ config:
91
126
 
92
127
  Any of the above settings can be specified using command-line arguments (`--app-context-message`, `--reference-language`, `--provider`, `--target-languages`). Command-line arguments take precedence.
93
128
 
129
+ ## Adding context
130
+ Sometimes a string isn't enough to give context to the AI, and as a result, it may give an undesirable translation. ALT allows you to specify additional context for this reason.
131
+ ### Application-level context
132
+ A global, application description can be specified `--app-context-message` (or `appContextMessage` in a [config](#config)).
133
+ For example, your config may include something like:
134
+ ```json
135
+ "appContextMessage": "Voided is a MMORPG game based on outer space."
136
+ ```
137
+ ### String-specific context
138
+ Context can be added for any reference key/value pairs by passing `--look-for-context-data` (or setting `lookForContextData: true` in a [config](#config)).
139
+
140
+ For example, given the following reference key/value pair:
141
+
142
+ ```json
143
+ "editor-add-component": '+ Star',
144
+ ```
145
+ This may not translate as desired, so ALT allows you to specify additional context in the form of another key/value pair. For example:
146
+ ```json
147
+ "_context:editor-add-component": "This is text for a button the galaxy UI, where a user can create a star"
148
+ ```
149
+ `_context:` can be whatever you prefer here. It's specified via `--context-prefix`, or `contextPrefix` in a [config](#config).
150
+
151
+ A suffix can be specified instead of (or in conjunction with) a prefix, with `--context-suffix`, or `contextSuffix` in a [config](#config). Example:
152
+ ```json
153
+ "editor-add-component[context]": "This is text for a button the graph editor"
154
+ ```
155
+ In this case, `[context]` would be specified by passing `--context-suffix '[context]'` or setting `"contextSuffix": "[context]"` in a [config](#config).
156
+
157
+ Further examples can be found [here](#examples).
158
+
159
+ ## Display language
160
+ ALT CLI itself has been localized so you can use it many languages. You can optionally set the display language with the `ALT_LANGUAGE` environment variable. Please feel free to submit
161
+ an issue if you do not see your preferred language.
162
+
94
163
  ## Usage
95
164
  ```
96
165
  Usage: alt [options] [command]
@@ -112,6 +181,7 @@ Environment variables:
112
181
  ALT_LANGUAGE POSIX locale used for display
113
182
 
114
183
  ---
184
+
115
185
  Usage: alt translate [options]
116
186
 
117
187
  Options:
@@ -141,6 +211,7 @@ Options:
141
211
  -h, --help display help for command
142
212
 
143
213
  ---
214
+
144
215
  Usage: alt list-models [options]
145
216
 
146
217
  Options:
@@ -213,11 +284,14 @@ Internally, there is currently nothing in the prompt about this. I've tested wit
213
284
 
214
285
  Please submit an issue if it causes you any trouble.
215
286
 
216
- ## Rules
217
- Translation will occur for a given target language & key if any of the following are true:
218
- * The reference value was modified and translation has not yet occurred for the given language/key
219
- * If a context value for the given target language/key is found and has been modified.
220
- * The `--force` flag is used
287
+ ## Translation rules
288
+ When does ALT translate a given source string? Translation will occur for a given target language & reference key/value if any of the following are true:
289
+ * The output file does not exist
290
+ * The output file is missing the reference key
291
+ * The reference value was modified
292
+ * A context value for the given target language/key is found and has been modified.
293
+ * `-f` or `--force` are specified
294
+ * The cache file (`.localization.cache.json`) is not present
221
295
 
222
296
  Translation will _not_ occur if `alt` detects that the given value in the target language file has been manually modified. If you modify an output value manually and want it to be re-translated
223
297
  later, you can just delete that key/value pair from the given file.
@@ -234,5 +308,5 @@ If you prefer to write updates to disk in real-time (anytime any output data cha
234
308
  ### CI
235
309
  You may want to use `--tty` for more useful output.
236
310
 
237
- ## Issues
311
+ ## PR
238
312
  Feel free to fix existing issues and submit a PR, or submit a new issue.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@drone1/alt",
4
- "version": "0.7.2",
4
+ "version": "0.8.0",
5
5
  "description": "An AI-powered localization tool",
6
6
  "main": "src/index.mjs",
7
7
  "bin": {
@@ -9,7 +9,8 @@
9
9
  },
10
10
  "scripts": {
11
11
  "localize-display-strings": "./alt.mjs translate --reference-file localization/reference.js",
12
- "print-all-help": "rm -f help.txt && (./alt.mjs help && echo -e '\n---\n' && ./alt.mjs help translate && echo -e '\n---\n' && ./alt.mjs help list-models) > help.txt"
12
+ "print-all-help": "rm -f help.txt && (./alt.mjs help && echo -e '\n---\n' && ./alt.mjs help translate && echo -e '\n---\n' && ./alt.mjs help list-models) > help.txt",
13
+ "generate-toc": "./scripts/gh-md-toc --insert README.md && rm -f README.md.orig.* README.md.toc.* && echo '\n**README.md updated with new table of contents**'"
13
14
  },
14
15
  "repository": {
15
16
  "type": "git",
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env bash
2
+
3
+ #
4
+ # Steps:
5
+ #
6
+ # 1. Download corresponding html file for some README.md:
7
+ # curl -s $1
8
+ #
9
+ # 2. Discard rows where no substring 'user-content-' (github's markup):
10
+ # awk '/user-content-/ { ...
11
+ #
12
+ # 3.1 Get last number in each row like ' ... </span></a>sitemap.js</h1'.
13
+ # It's a level of the current header:
14
+ # substr($0, length($0), 1)
15
+ #
16
+ # 3.2 Get level from 3.1 and insert corresponding number of spaces before '*':
17
+ # sprintf("%*s", (level-1)*'"$nb_spaces"', "")
18
+ #
19
+ # 4. Find head's text and insert it inside "* [ ... ]":
20
+ # substr($0, match($0, /a>.*<\/h/)+2, RLENGTH-5)
21
+ #
22
+ # 5. Find anchor and insert it inside "(...)":
23
+ # substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8)
24
+ #
25
+
26
+ gh_toc_version="0.10.0"
27
+
28
+ gh_user_agent="gh-md-toc v$gh_toc_version"
29
+
30
+ #
31
+ # Download rendered into html README.md by its url.
32
+ #
33
+ #
34
+ gh_toc_load() {
35
+ local gh_url=$1
36
+
37
+ if type curl &>/dev/null; then
38
+ curl --user-agent "$gh_user_agent" -s "$gh_url"
39
+ elif type wget &>/dev/null; then
40
+ wget --user-agent="$gh_user_agent" -qO- "$gh_url"
41
+ else
42
+ echo "Please, install 'curl' or 'wget' and try again."
43
+ exit 1
44
+ fi
45
+ }
46
+
47
+ #
48
+ # Converts local md file into html by GitHub
49
+ #
50
+ # -> curl -X POST --data '{"text": "Hello world github/linguist#1 **cool**, and #1!"}' https://api.github.com/markdown
51
+ # <p>Hello world github/linguist#1 <strong>cool</strong>, and #1!</p>'"
52
+ gh_toc_md2html() {
53
+ local gh_file_md=$1
54
+ local skip_header=$2
55
+
56
+ URL=https://api.github.com/markdown/raw
57
+
58
+ if [ -n "$GH_TOC_TOKEN" ]; then
59
+ TOKEN=$GH_TOC_TOKEN
60
+ else
61
+ TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt"
62
+ if [ -f "$TOKEN_FILE" ]; then
63
+ TOKEN="$(cat "$TOKEN_FILE")"
64
+ fi
65
+ fi
66
+ if [ -n "${TOKEN}" ]; then
67
+ AUTHORIZATION="Authorization: token ${TOKEN}"
68
+ fi
69
+
70
+ local gh_tmp_file_md=$gh_file_md
71
+ if [ "$skip_header" = "yes" ]; then
72
+ if grep -Fxq "<!--te-->" "$gh_src"; then
73
+ # cut everything before the toc
74
+ gh_tmp_file_md=$gh_file_md~~
75
+ sed '1,/<!--te-->/d' "$gh_file_md" > "$gh_tmp_file_md"
76
+ fi
77
+ fi
78
+
79
+ # echo $URL 1>&2
80
+ OUTPUT=$(curl -s \
81
+ --user-agent "$gh_user_agent" \
82
+ --data-binary @"$gh_tmp_file_md" \
83
+ -H "Content-Type:text/plain" \
84
+ -H "$AUTHORIZATION" \
85
+ "$URL")
86
+
87
+ rm -f "${gh_file_md}~~"
88
+
89
+ if [ "$?" != "0" ]; then
90
+ echo "XXNetworkErrorXX"
91
+ fi
92
+ if [ "$(echo "${OUTPUT}" | awk '/API rate limit exceeded/')" != "" ]; then
93
+ echo "XXRateLimitXX"
94
+ else
95
+ echo "${OUTPUT}"
96
+ fi
97
+ }
98
+
99
+
100
+ #
101
+ # Is passed string url
102
+ #
103
+ gh_is_url() {
104
+ case $1 in
105
+ https* | http*)
106
+ echo "yes";;
107
+ *)
108
+ echo "no";;
109
+ esac
110
+ }
111
+
112
+ #
113
+ # TOC generator
114
+ #
115
+ gh_toc(){
116
+ local gh_src=$1
117
+ local gh_src_copy=$1
118
+ local gh_ttl_docs=$2
119
+ local need_replace=$3
120
+ local no_backup=$4
121
+ local no_footer=$5
122
+ local indent=$6
123
+ local skip_header=$7
124
+
125
+ if [ "$gh_src" = "" ]; then
126
+ echo "Please, enter URL or local path for a README.md"
127
+ exit 1
128
+ fi
129
+
130
+
131
+ # Show "TOC" string only if working with one document
132
+ if [ "$gh_ttl_docs" = "1" ]; then
133
+
134
+ echo "Table of Contents"
135
+ echo "================="
136
+ echo ""
137
+ gh_src_copy=""
138
+
139
+ fi
140
+
141
+ if [ "$(gh_is_url "$gh_src")" == "yes" ]; then
142
+ gh_toc_load "$gh_src" | gh_toc_grab "$gh_src_copy" "$indent"
143
+ if [ "${PIPESTATUS[0]}" != "0" ]; then
144
+ echo "Could not load remote document."
145
+ echo "Please check your url or network connectivity"
146
+ exit 1
147
+ fi
148
+ if [ "$need_replace" = "yes" ]; then
149
+ echo
150
+ echo "!! '$gh_src' is not a local file"
151
+ echo "!! Can't insert the TOC into it."
152
+ echo
153
+ fi
154
+ else
155
+ local rawhtml
156
+ rawhtml=$(gh_toc_md2html "$gh_src" "$skip_header")
157
+ if [ "$rawhtml" == "XXNetworkErrorXX" ]; then
158
+ echo "Parsing local markdown file requires access to github API"
159
+ echo "Please make sure curl is installed and check your network connectivity"
160
+ exit 1
161
+ fi
162
+ if [ "$rawhtml" == "XXRateLimitXX" ]; then
163
+ echo "Parsing local markdown file requires access to github API"
164
+ echo "Error: You exceeded the hourly limit. See: https://developer.github.com/v3/#rate-limiting"
165
+ TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt"
166
+ echo "or place GitHub auth token here: ${TOKEN_FILE}"
167
+ exit 1
168
+ fi
169
+ local toc
170
+ toc=`echo "$rawhtml" | gh_toc_grab "$gh_src_copy" "$indent"`
171
+ echo "$toc"
172
+ if [ "$need_replace" = "yes" ]; then
173
+ if grep -Fxq "<!--ts-->" "$gh_src" && grep -Fxq "<!--te-->" "$gh_src"; then
174
+ echo "Found markers"
175
+ else
176
+ echo "You don't have <!--ts--> or <!--te--> in your file...exiting"
177
+ exit 1
178
+ fi
179
+ local ts="<\!--ts-->"
180
+ local te="<\!--te-->"
181
+ local dt
182
+ dt=$(date +'%F_%H%M%S')
183
+ local ext=".orig.${dt}"
184
+ local toc_path="${gh_src}.toc.${dt}"
185
+ local toc_createdby="<!-- Created by https://github.com/ekalinin/github-markdown-toc -->"
186
+ local toc_footer
187
+ toc_footer="<!-- Added by: `whoami`, at: `date` -->"
188
+ # http://fahdshariff.blogspot.ru/2012/12/sed-mutli-line-replacement-between-two.html
189
+ # clear old TOC
190
+ sed -i"${ext}" "/${ts}/,/${te}/{//!d;}" "$gh_src"
191
+ # create toc file
192
+ echo "${toc}" > "${toc_path}"
193
+ if [ "${no_footer}" != "yes" ]; then
194
+ echo -e "\n${toc_createdby}\n${toc_footer}\n" >> "$toc_path"
195
+ fi
196
+
197
+ # insert toc file
198
+ if ! sed --version > /dev/null 2>&1; then
199
+ sed -i "" "/${ts}/r ${toc_path}" "$gh_src"
200
+ else
201
+ sed -i "/${ts}/r ${toc_path}" "$gh_src"
202
+ fi
203
+ echo
204
+ if [ "${no_backup}" = "yes" ]; then
205
+ rm "$toc_path" "$gh_src$ext"
206
+ fi
207
+ echo "!! TOC was added into: '$gh_src'"
208
+ if [ -z "${no_backup}" ]; then
209
+ echo "!! Origin version of the file: '${gh_src}${ext}'"
210
+ echo "!! TOC added into a separate file: '${toc_path}'"
211
+ fi
212
+ echo
213
+ fi
214
+ fi
215
+ }
216
+
217
+ #
218
+ # Grabber of the TOC from rendered html
219
+ #
220
+ # $1 - a source url of document.
221
+ # It's need if TOC is generated for multiple documents.
222
+ # $2 - number of spaces used to indent.
223
+ #
224
+ gh_toc_grab() {
225
+
226
+ href_regex="/href=\"[^\"]+?\"/"
227
+ common_awk_script='
228
+ modified_href = ""
229
+ split(href, chars, "")
230
+ for (i=1;i <= length(href); i++) {
231
+ c = chars[i]
232
+ res = ""
233
+ if (c == "+") {
234
+ res = " "
235
+ } else {
236
+ if (c == "%") {
237
+ res = "\\x"
238
+ } else {
239
+ res = c ""
240
+ }
241
+ }
242
+ modified_href = modified_href res
243
+ }
244
+ print sprintf("%*s", (level-1)*'"$2"', "") "* [" text "](" gh_url modified_href ")"
245
+ '
246
+ if [ "`uname -s`" == "OS/390" ]; then
247
+ grepcmd="pcregrep -o"
248
+ echoargs=""
249
+ awkscript='{
250
+ level = substr($0, 3, 1)
251
+ text = substr($0, match($0, /<\/span><\/a>[^<]*<\/h/)+11, RLENGTH-14)
252
+ href = substr($0, match($0, '$href_regex')+6, RLENGTH-7)
253
+ '"$common_awk_script"'
254
+ }'
255
+ else
256
+ grepcmd="grep -Eo"
257
+ echoargs="-e"
258
+ awkscript='{
259
+ level = substr($0, 3, 1)
260
+ text = substr($0, match($0, /">.*<\/h/)+2, RLENGTH-5)
261
+ href = substr($0, match($0, '$href_regex')+6, RLENGTH-7)
262
+ '"$common_awk_script"'
263
+ }'
264
+ fi
265
+
266
+ # if closed <h[1-6]> is on the new line, then move it on the prev line
267
+ # for example:
268
+ # was: The command <code>foo1</code>
269
+ # </h1>
270
+ # became: The command <code>foo1</code></h1>
271
+ sed -e ':a' -e 'N' -e '$!ba' -e 's/\n<\/h/<\/h/g' |
272
+
273
+ # Sometimes a line can start with <span>. Fix that.
274
+ sed -e ':a' -e 'N' -e '$!ba' -e 's/\n<span/<span/g' |
275
+
276
+ # find strings that corresponds to template
277
+ $grepcmd '<h.*class="heading-element".*</a' |
278
+
279
+ # remove code tags
280
+ sed 's/<code>//g' | sed 's/<\/code>//g' |
281
+
282
+ # remove g-emoji
283
+ sed 's/<g-emoji[^>]*[^<]*<\/g-emoji> //g' |
284
+
285
+ # now all rows are like:
286
+ # <h1 class="heading-element">title</h1><a href="..."><span>..</span></a>
287
+ # format result line
288
+ # * $0 - whole string
289
+ # * last element of each row: "</hN" where N in (1,2,3,...)
290
+ echo $echoargs "$(awk -v "gh_url=$1" "$awkscript")"
291
+ }
292
+
293
+ # perl -lpE 's/(\[[^\]]*\]\()(.*?)(\))/my ($pre, $in, $post)=($1, $2, $3) ; $in =~ s{\+}{ }g; $in =~ s{%}{\\x}g; $pre.$in.$post/ems')"
294
+
295
+ #
296
+ # Returns filename only from full path or url
297
+ #
298
+ gh_toc_get_filename() {
299
+ echo "${1##*/}"
300
+ }
301
+
302
+ show_version() {
303
+ echo "$gh_toc_version"
304
+ echo
305
+ echo "os: `uname -s`"
306
+ echo "arch: `uname -m`"
307
+ echo "kernel: `uname -r`"
308
+ echo "shell: `$SHELL --version`"
309
+ echo
310
+ for tool in curl wget grep awk sed; do
311
+ printf "%-5s: " $tool
312
+ if type $tool &>/dev/null; then
313
+ $tool --version | head -n 1
314
+ else
315
+ echo "not installed"
316
+ fi
317
+ done
318
+ }
319
+
320
+ show_help() {
321
+ local app_name
322
+ app_name=$(basename "$0")
323
+ echo "GitHub TOC generator ($app_name): $gh_toc_version"
324
+ echo ""
325
+ echo "Usage:"
326
+ echo " $app_name [options] src [src] Create TOC for a README file (url or local path)"
327
+ echo " $app_name - Create TOC for markdown from STDIN"
328
+ echo " $app_name --help Show help"
329
+ echo " $app_name --version Show version"
330
+ echo ""
331
+ echo "Options:"
332
+ echo " --indent <NUM> Set indent size. Default: 3."
333
+ echo " --insert Insert new TOC into original file. For local files only. Default: false."
334
+ echo " See https://github.com/ekalinin/github-markdown-toc/issues/41 for details."
335
+ echo " --no-backup Remove backup file. Set --insert as well. Default: false."
336
+ echo " --hide-footer Do not write date & author of the last TOC update. Set --insert as well. Default: false."
337
+ echo " --skip-header Hide entry of the topmost headlines. Default: false."
338
+ echo " See https://github.com/ekalinin/github-markdown-toc/issues/125 for details."
339
+ echo ""
340
+ }
341
+
342
+ #
343
+ # Options handlers
344
+ #
345
+ gh_toc_app() {
346
+ local need_replace="no"
347
+ local indent=3
348
+
349
+ if [ "$1" = '--help' ] || [ $# -eq 0 ] ; then
350
+ show_help
351
+ return
352
+ fi
353
+
354
+ if [ "$1" = '--version' ]; then
355
+ show_version
356
+ return
357
+ fi
358
+
359
+ if [ "$1" = '--indent' ]; then
360
+ indent="$2"
361
+ shift 2
362
+ fi
363
+
364
+ if [ "$1" = "-" ]; then
365
+ if [ -z "$TMPDIR" ]; then
366
+ TMPDIR="/tmp"
367
+ elif [ -n "$TMPDIR" ] && [ ! -d "$TMPDIR" ]; then
368
+ mkdir -p "$TMPDIR"
369
+ fi
370
+ local gh_tmp_md
371
+ if [ "`uname -s`" == "OS/390" ]; then
372
+ local timestamp
373
+ timestamp=$(date +%m%d%Y%H%M%S)
374
+ gh_tmp_md="$TMPDIR/tmp.$timestamp"
375
+ else
376
+ gh_tmp_md=$(mktemp "$TMPDIR/tmp.XXXXXX")
377
+ fi
378
+ while read -r input; do
379
+ echo "$input" >> "$gh_tmp_md"
380
+ done
381
+ gh_toc_md2html "$gh_tmp_md" | gh_toc_grab "" "$indent"
382
+ return
383
+ fi
384
+
385
+ if [ "$1" = '--insert' ]; then
386
+ need_replace="yes"
387
+ shift
388
+ fi
389
+
390
+ if [ "$1" = '--no-backup' ]; then
391
+ need_replace="yes"
392
+ no_backup="yes"
393
+ shift
394
+ fi
395
+
396
+ if [ "$1" = '--hide-footer' ]; then
397
+ need_replace="yes"
398
+ no_footer="yes"
399
+ shift
400
+ fi
401
+
402
+ if [ "$1" = '--skip-header' ]; then
403
+ skip_header="yes"
404
+ shift
405
+ fi
406
+
407
+
408
+ for md in "$@"
409
+ do
410
+ echo ""
411
+ gh_toc "$md" "$#" "$need_replace" "$no_backup" "$no_footer" "$indent" "$skip_header"
412
+ done
413
+
414
+ echo ""
415
+ echo "<!-- Created by https://github.com/ekalinin/github-markdown-toc -->"
416
+ }
417
+
418
+ #
419
+ # Entry point
420
+ #
421
+ gh_toc_app "$@"
@@ -33,7 +33,7 @@ export async function runTranslation({ appState, options, log }) {
33
33
  })
34
34
 
35
35
  // Validate provider
36
- const providerName = options.provider ?? config.provider
36
+ const providerName = (options.provider ?? config.provider)?.toLowerCase()
37
37
  if (!VALID_TRANSLATION_PROVIDERS.includes(providerName)) {
38
38
  log.E(`Error: Unknown provider "${providerName}". Supported providers: ${VALID_TRANSLATION_PROVIDERS.join(', ')}`)
39
39
  process.exit(2)
@@ -305,7 +305,7 @@ export async function runTranslation({ appState, options, log }) {
305
305
  }
306
306
  ], {
307
307
  concurrent: false, // Process languages one by one
308
- ...(options.tty ? { renderer: 'simple' } : {}),
308
+ ...((options.tty || options.trace || options.debug || options.verbose) ? { renderer: 'simple' } : {}),
309
309
  rendererOptions: { collapse: false, clearOutput: false },
310
310
  registerSignalListeners: true,
311
311
  collapseSubtasks: false
@@ -395,7 +395,7 @@ export async function processTranslationTask({ appState, taskInfo, listrTask, op
395
395
  log.V(`Keeping existing translation and hash for ${targetLang}/${key}...`)
396
396
 
397
397
  // Allow the user to directly edit/tweak output key values
398
- listrTask.output = localizeFormatted({ token: 'msg-no-updated-needed-for-key', data: { key }, lang: appState.lang, log })
398
+ listrTask.output = localizeFormatted({ token: 'msg-no-update-needed-for-key', data: { key }, lang: appState.lang, log })
399
399
  }
400
400
  }
401
401
 
@@ -580,7 +580,7 @@ async function translateTextViaProvider({
580
580
  }
581
581
 
582
582
  if (!errorHandled) {
583
- log.W(`${providerName} API failed. Error:`, error?.message ?? error)
583
+ log.W(`${providerName} API failed.`, error?.message ?? error)
584
584
  }
585
585
  }
586
586
  }
package/src/lib/consts.js CHANGED
@@ -6,12 +6,14 @@ export const LANGTAG_DEFAULT = LANGTAG_ENGLISH
6
6
 
7
7
  export const VALID_TRANSLATION_PROVIDERS = [
8
8
  'anthropic',
9
+ 'google',
9
10
  'openai'
10
11
  ]
11
12
 
12
13
  export const ENV_VARS = [
13
14
  { name: 'ANTHROPIC_API_KEY', description: 'Your Anthropic API key' },
14
15
  { name: 'OPENAI_API_KEY', description: 'Your OpenAI API key' },
16
+ { name: 'GOOGLE_API_KEY', description: 'Your Google Gemini API key' },
15
17
  { name: 'ALT_LANGUAGE', description: 'POSIX locale used for display' }
16
18
  ]
17
19
 
@@ -30,6 +32,7 @@ export const SUPPORTED_REFERENCE_FILE_EXTENSIONS = [
30
32
 
31
33
  export const DEFAULT_LLM_MODELS = {
32
34
  anthropic: 'claude-3-7-sonnet-20250219',
35
+ google: 'gemini-2.0-flash',
33
36
  openai: 'gpt-4-turbo'
34
37
  }
35
38
 
package/src/main.mjs CHANGED
@@ -125,9 +125,9 @@ export async function run() {
125
125
  .option('-cp, --context-prefix <value>', `String to be prefixed to all keys to search for additional context, which are passed along to the AI for context`)
126
126
  .option('-cs, --context-suffix <value>', `String to be suffixed to all keys to search for additional context, which are passed along to the AI for context`)
127
127
  .option('-L, --look-for-context-data', `If specified, ALT will pass any context data specified in the reference file to the AI provider for translation. At least one of --contextPrefix or --contextSuffix must be specified`, false)
128
- .option('-v, --verbose', `Enables verbose spew`, false)
129
- .option('-d, --debug', `Enables debug spew`, false)
130
- .option('-t, --trace', `Enables trace spew`, false)
128
+ .option('-v, --verbose', `Enables verbose spew; forces --tty mode`, false)
129
+ .option('-d, --debug', `Enables debug spew; forces --tty mode`, false)
130
+ .option('-t, --trace', `Enables trace spew; forces --tty mode`, false)
131
131
  .option('--dev', `Enable dev mode, which prints stack traces with errors`, false)
132
132
  .hook('preAction', cmd => {
133
133
  const opts = cmd.opts()
@@ -0,0 +1,80 @@
1
+ export function name() {
2
+ return 'Google'
3
+ }
4
+
5
+ export async function listModels(apiKey) {
6
+ let allModels = []
7
+ let pageToken = null
8
+ let hasMore = true
9
+
10
+ while (hasMore) {
11
+ const url = new URL('https://generativelanguage.googleapis.com/v1/models')
12
+
13
+ // Add API key as query parameter
14
+ url.searchParams.append('key', apiKey)
15
+
16
+ // Add page token if we have one
17
+ if (pageToken) {
18
+ url.searchParams.append('pageToken', pageToken)
19
+ }
20
+
21
+ const response = await fetch(url, {
22
+ method: 'GET',
23
+ headers: {
24
+ 'Content-Type': 'application/json'
25
+ }
26
+ })
27
+
28
+ const result = await response.json()
29
+
30
+ if (result.models && result.models.length > 0) {
31
+ allModels = [
32
+ ...allModels,
33
+ ...result.models
34
+ ]
35
+
36
+ // Check if there's another page
37
+ if (result.nextPageToken) {
38
+ pageToken = result.nextPageToken
39
+ } else {
40
+ hasMore = false
41
+ }
42
+ } else {
43
+ hasMore = false
44
+ }
45
+ }
46
+
47
+ return { models: allModels }
48
+ }
49
+
50
+ export function getTranslationRequestDetails({ model, messages, apiKey, log }) {
51
+ return {
52
+ url: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`,
53
+ params: {
54
+ contents: messages.map(m => ({
55
+ role: 'user',
56
+ parts: [ { text: m } ]
57
+ }))
58
+ },
59
+ config: {
60
+ headers: {
61
+ 'Content-Type': 'application/json'
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ export function getResult(response, log) {
68
+ return response.data.candidates?.[0]?.content?.parts?.[0]?.text?.trim?.() || ''
69
+ }
70
+
71
+ function getHeader(headers, name) {
72
+ return headers[name] || headers.get?.(name)
73
+ }
74
+
75
+ export function getSleepInterval(headers, log) {
76
+ log.T(headers)
77
+ const retryAfter = parseInt(getHeader(headers, 'retry-after'))
78
+ log.D('retryAfter', retryAfter)
79
+ return isNaN(retryAfter) ? 0 : 1000 * retryAfter + 200
80
+ }
@@ -0,0 +1,28 @@
1
+ export default {
2
+ 'msg-nothing-to-do': `Nothing to do`,
3
+ 'msg-finished-with-errors': `Finished with %%errorsEncountered%% error%%s%%`,
4
+
5
+ 'msg-translating': 'Translating...',
6
+ 'msg-translating-key': `Translating %%key%%`,
7
+ 'msg-preparing-endpoint-config': `Preparing endpoint configuration...`,
8
+ 'msg-hitting-provider-endpoint': `Hitting %%providerName%% endpoint%%attemptStr%%...`,
9
+
10
+ 'msg-no-update-needed-for-key': `No update needed for %%key%%`,
11
+ 'msg-rate-limited-sleeping': `Rate limited; sleeping for %%interval%%s...%%attemptStr%%`,
12
+ 'msg-show-translation-result': `Translated %%key%%: "%%newValue%%"`,
13
+ 'msg-processing-lang-and-key': `[%%progress%%%] Processing %%targetLang%% – %%key%%...`,
14
+
15
+ 'msg-translation-reason-forced': `Forced update`,
16
+ 'msg-translation-reason-outputFileDidNotExist': `Output file %%outputFile%% did not exist`,
17
+ 'msg-translation-reason-userMissingReferenceValueHash': `No reference hash found`,
18
+ 'msg-translation-reason-userModifiedReferenceValue': `User modified reference string`,
19
+ 'msg-translation-reason-missingOutputKey': `No existing translation found`,
20
+ 'msg-translation-reason-missingOutputValueHash': `No hash found in cache file`,
21
+
22
+ 'error-value-not-a-string': `Value for reference key "%%key%%" was "%%type%%". Expected a string! Skipping...`,
23
+ 'error-value-not-in-reference-data': `Key "%%key%%" did not exist in reference file`,
24
+ 'error-translation-failed': `Translation failed for target language=%%targetLang%%; key=%%key%%; text=%%refValue%%`,
25
+ 'error-bad-reference-file-ext': `Unsupported file type for reference file "%%ext%%"`,
26
+ 'error-reference-var-not-found-in-data': `Couldn't find "%%referenceExportedVarName%%" in reference file "%%referenceFile%%". Did you mean one of these instead?: %%possibleKeys%%`,
27
+ 'error-reference-file-load-failed': `Failed to load reference file "%%referenceFile%%"`
28
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "msg-nothing-to-do": "Nothing to do",
3
+ "msg-finished-with-errors": "Finished with %%errorsEncountered%% error%%s%%",
4
+
5
+ "msg-translating": "Translating...",
6
+ "msg-translating-key": "Translating %%key%%",
7
+ "msg-preparing-endpoint-config": "Preparing endpoint configuration...",
8
+ "msg-hitting-provider-endpoint": "Hitting %%providerName%% endpoint%%attemptStr%%...",
9
+
10
+ "msg-no-update-needed-for-key": "No update needed for %%key%%",
11
+ "msg-rate-limited-sleeping": "Rate limited; sleeping for %%interval%%s...%%attemptStr%%",
12
+ "msg-show-translation-result": "Translated %%key%%: \"%%newValue%%\"",
13
+ "msg-processing-lang-and-key": "[%%progress%%%] Processing %%targetLang%% – %%key%%...",
14
+
15
+ "msg-translation-reason-forced": "Forced update",
16
+ "msg-translation-reason-outputFileDidNotExist": "Output file %%outputFile%% did not exist",
17
+ "msg-translation-reason-userMissingReferenceValueHash": "No reference hash found",
18
+ "msg-translation-reason-userModifiedReferenceValue": "User modified reference string",
19
+ "msg-translation-reason-missingOutputKey": "No existing translation found",
20
+ "msg-translation-reason-missingOutputValueHash": "No hash found in cache file",
21
+
22
+ "error-value-not-a-string": "Value for reference key \"%%key%%\" was \"%%type%%\". Expected a string! Skipping...",
23
+ "error-value-not-in-reference-data": "Key \"%%key%%\" did not exist in reference file",
24
+ "error-translation-failed": "Translation failed for target language=%%targetLang%%; key=%%key%%; text=%%refValue%%",
25
+ "error-bad-reference-file-ext": "Unsupported file type for reference file \"%%ext%%\"",
26
+ "error-reference-var-not-found-in-data": "Couldn't find \"%%referenceExportedVarName%%\" in reference file \"%%referenceFile%%\". Did you mean one of these instead?: %%possibleKeys%%",
27
+ "error-reference-file-load-failed": "Failed to load reference file \"%%referenceFile%%\""
28
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "msg-nothing-to-do": "Nothing to do",
3
+ "msg-finished-with-errors": "Finished with %%errorsEncountered%% error%%s%%",
4
+
5
+ "msg-translating": "Translating...",
6
+ "msg-translating-key": "Translating %%key%%",
7
+ "msg-preparing-endpoint-config": "Preparing endpoint configuration...",
8
+ "msg-hitting-provider-endpoint": "Hitting %%providerName%% endpoint%%attemptStr%%...",
9
+
10
+ "msg-no-update-needed-for-key": "No update needed for %%key%%",
11
+ "msg-rate-limited-sleeping": "Rate limited; sleeping for %%interval%%s...%%attemptStr%%",
12
+ "msg-show-translation-result": "Translated %%key%%: \"%%newValue%%\"",
13
+ "msg-processing-lang-and-key": "[%%progress%%%] Processing %%targetLang%% – %%key%%...",
14
+
15
+ "msg-translation-reason-forced": "Forced update",
16
+ "msg-translation-reason-outputFileDidNotExist": "Output file %%outputFile%% did not exist",
17
+ "msg-translation-reason-userMissingReferenceValueHash": "No reference hash found",
18
+ "msg-translation-reason-userModifiedReferenceValue": "User modified reference string",
19
+ "msg-translation-reason-missingOutputKey": "No existing translation found",
20
+ "msg-translation-reason-missingOutputValueHash": "No hash found in cache file",
21
+
22
+ // Errors
23
+ "error-value-not-a-string": "Value for reference key \"%%key%%\" was \"%%type%%\". Expected a string! Skipping...",
24
+ "error-value-not-in-reference-data": "Key \"%%key%%\" did not exist in reference file",
25
+ "error-translation-failed": "Translation failed for target language=%%targetLang%%; key=%%key%%; text=%%refValue%%",
26
+ "error-bad-reference-file-ext": "Unsupported file type for reference file \"%%ext%%\"",
27
+ "error-reference-var-not-found-in-data": "Couldn't find \"%%referenceExportedVarName%%\" in reference file \"%%referenceFile%%\". Did you mean one of these instead?: %%possibleKeys%%",
28
+ "error-reference-file-load-failed": "Failed to load reference file \"%%referenceFile%%\""
29
+ }