├── apt-packages.txt ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── bug-提交-简体中文-.md │ └── bug-提交-繁體中文-.md ├── copilot-instructions.md ├── dependabot.yml └── workflows │ ├── remove_dup.yml │ ├── update_contributors_images.yml │ ├── locals(greasyfork).js_update.yaml │ └── main.user.js_version_update_and_sync_zh-TW.yaml ├── preview ├── img1.png ├── img2.png ├── img3.png └── img4.png ├── .gitignore ├── t2s_rules.conf ├── .editorconfig ├── script ├── rd.py ├── t2s-convert.pl └── ci_commit_with_signature.sh ├── README_zh-TW.md ├── README.md ├── main(greasyfork).user.js ├── main_zh-TW.user.js ├── main.user.js └── LICENSE /apt-packages.txt: -------------------------------------------------------------------------------- 1 | opencc 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | When performing a code review, respond in Chinese. 2 | -------------------------------------------------------------------------------- /preview/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maboloshi/github-chinese/HEAD/preview/img1.png -------------------------------------------------------------------------------- /preview/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maboloshi/github-chinese/HEAD/preview/img2.png -------------------------------------------------------------------------------- /preview/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maboloshi/github-chinese/HEAD/preview/img3.png -------------------------------------------------------------------------------- /preview/img4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maboloshi/github-chinese/HEAD/preview/img4.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | 3 | # windows thumbnail cache 4 | Thumbs.db 5 | Desktop.ini 6 | 7 | # Mac DS_Store Files 8 | .DS_Store 9 | 10 | # webstorm 11 | .idea 12 | 13 | main(local).js -------------------------------------------------------------------------------- /t2s_rules.conf: -------------------------------------------------------------------------------- 1 | # 格式说明: 2 | # 普通替换:源文本(2+空格)目标文本 3 | # 正则替换:REGEX:模式(2+空格)替换文本 4 | zh-CN zh-TW 5 | 隻讀 只讀 6 | 隻影響 只影響 7 | 隻是 只是 8 | 復刻 複刻 9 | 軟件 軟體 10 | 代碼 程式碼 11 | 程序 程式 12 | 項目 專案 13 | 併合並 並合併 14 | 合並 合併 15 | 復雜 複雜 16 | 提交併 提交並 17 | 髮送 發送 18 | REGEX:(?' 34 | closeDelimiter='' 35 | sed -i "/$openDelimiter/,/$closeDelimiter/c\\$openDelimiter$escapedHtmlList$closeDelimiter" README.md 36 | git diff --quiet --exit-code README.md || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV 37 | 38 | - name: Commit and push README.md 39 | if: ${{ env.CHANGES_DETECTED == 'true' }} 40 | env: 41 | GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 42 | APP_SLUG: ${{ steps.generate_token.outputs.app-slug }} 43 | run: | 44 | bash script/ci_commit_with_signature.sh \ 45 | -R "${{ github.repository }}" \ 46 | -B "${{ github.ref_name }}" \ 47 | -P "${{ github.sha }}" \ 48 | -F "README.md" \ 49 | -h "修改文档: 更新\`贡献者列表\`" 50 | -------------------------------------------------------------------------------- /.github/workflows/locals(greasyfork).js_update.yaml: -------------------------------------------------------------------------------- 1 | name: main(greasyfork).user.js and locals(greasyfork).js update 2 | on: 3 | schedule: 4 | # 每周四的UTC时间16点(北京时间周五凌晨0点) 5 | - cron: "0 16 * * 4" 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | update_version: 13 | runs-on: ubuntu-latest 14 | environment: github-pages 15 | steps: 16 | - name: Generate a token 17 | id: generate_token 18 | uses: actions/create-github-app-token@v2 19 | with: 20 | app-id: ${{ vars.APP_ID }} 21 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 22 | 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Sync update locals(greasyfork).js 29 | run: | 30 | # 获得最近以`main.user.js Update to version`开头的提交的引用 31 | ref=$(git log --grep="^main.user.js Update to" -n 1 --pretty=format:"%H") 32 | 33 | # 获取该提交中`locals.js`的文件时间, 并设置环境变量 34 | locals_js_time=$(git log -1 --format=%at $ref -- locals.js) 35 | echo "locals_js_time=$locals_js_time" >> $GITHUB_ENV 36 | 37 | # 从该提交中提取文件`locals.js`并覆写到本地文件`locals(greasyfork).js` 38 | git show $ref:locals.js > "locals(greasyfork).js" 39 | 40 | git diff --quiet --exit-code "locals(greasyfork).js" || \ 41 | echo "LOCALS_JS_CHANGED=true" >> $GITHUB_ENV 42 | 43 | - name: Update version in main(greasyfork).user.js 44 | if: | 45 | ${{ env.LOCALS_JS_CHANGED == 'true' }} 46 | run: | 47 | # 使用`locals.js`的文件日期为词库版本号 48 | locals_js_date=$(TZ='Asia/Shanghai' date -d "@${{ env.locals_js_time }}" +'%Y-%m-%d') 49 | sed -i -E " 50 | s/(@version\s*([0-9]+\.){2}[0-9]+)(-[0-9]{4}-[0-9]{2}-[0-9]{2})?/\1-$locals_js_date/ 51 | s/(@require.+435207-github-.+.js\?v([0-9]+\.){2}[0-9]+)(-[0-9]{4}-[0-9]{2}-[0-9]{2})?/\1-$locals_js_date/ 52 | " "main(greasyfork).user.js" 53 | 54 | git diff --quiet --exit-code "main(greasyfork).user.js" || \ 55 | echo "MAIN_USER_JS_CHANGED=true" >> $GITHUB_ENV 56 | 57 | - name: Commit and push locals(greasyfork).js and main(greasyfork).user.js 58 | if: ${{ env.LOCALS_JS_CHANGED == 'true' && 59 | env.MAIN_USER_JS_CHANGED == 'true' }} 60 | env: 61 | GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 62 | APP_SLUG: ${{ steps.generate_token.outputs.app-slug }} 63 | run: | 64 | version=$(sed -n -E 's/^\/\/ @version\s+(.+)/\1/p' "main(greasyfork).user.js") 65 | bash script/ci_commit_with_signature.sh \ 66 | -R "${{ github.repository }}" \ 67 | -B "${{ github.ref_name }}" \ 68 | -P "${{ github.sha }}" \ 69 | -F "locals(greasyfork).js, main(greasyfork).user.js" \ 70 | -h "main(greasyfork).user.js Sync Thesaurus to $version" 71 | -------------------------------------------------------------------------------- /script/t2s-convert.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use strict; 3 | use warnings; 4 | use utf8; 5 | use open ':std', ':encoding(UTF-8)'; 6 | use Getopt::Long; 7 | use Cwd qw(abs_path); 8 | use File::Copy qw(move); 9 | 10 | # 配置参数 11 | my %opt = ( 12 | rules => 't2s_rules.conf', # 默认规则文件 13 | backup => 0, # 是否备份 14 | verbose => 0, # 显示详细信息 15 | encoding => 'UTF-8', # 文件编码 16 | output => undef, # 输出目录或文件 17 | 18 | ); 19 | GetOptions( 20 | 'r|rules=s' => \$opt{rules}, 21 | 'b|backup' => \$opt{backup}, 22 | 'v|verbose' => \$opt{verbose}, 23 | 'e|encoding=s' => \$opt{encoding}, 24 | 'o|output=s' => \$opt{output}, 25 | 'h|help' => sub { usage() }, 26 | ) or usage(1); 27 | 28 | # 参数校验 29 | usage(1) unless @ARGV; 30 | -e $opt{rules} or die "[ERROR] 规则文件 '$opt{rules}' 不存在\n"; 31 | 32 | # 预加载规则 33 | my @rules; 34 | load_rules($opt{rules}, \@rules); 35 | 36 | # 处理文件 37 | foreach my $file (@ARGV) { 38 | convert_file($file, \@rules); 39 | } 40 | 41 | #=============== 子函数 ================ 42 | sub load_rules { 43 | my ($rule_file, $rules_ref) = @_; 44 | 45 | open(my $fh, '<:encoding(UTF-8)', $rule_file) 46 | or die "[ERROR] 无法打开规则文件 '$rule_file': $!\n"; 47 | 48 | while (<$fh>) { 49 | chomp; 50 | next if /^\s*#/ || /^\s*$/; # 跳过注释和空行 51 | 52 | # 解析规则行(支持2+空格对齐和REGEX:标记) 53 | my ($is_regex, $pattern, $replacement) = parse_rule_line($_); 54 | 55 | # 编译正则表达式 56 | my $compiled = $is_regex ? qr/$pattern/ : qr/\Q$pattern\E/; 57 | push @$rules_ref, { 58 | pattern => $compiled, 59 | replace => $replacement, 60 | raw => $_ 61 | }; 62 | 63 | print "[RULE] 已加载: $_\n" if $opt{verbose}; 64 | } 65 | close $fh; 66 | } 67 | 68 | sub parse_rule_line { 69 | my ($line) = @_; 70 | 71 | my $is_regex = ($line =~ s/^REGEX://) ? 1 : 0; # 识别正则表达式标记 72 | my ($p, $r) = split(/\s{2,}/, $line, 2); # 提取参数,支持2+空格分隔 73 | 74 | unless (defined $p && defined $r) { 75 | my $type = $is_regex ? '(REGEX)' : ''; 76 | die "[ERROR] 规则格式错误$type: '$line'\n"; 77 | } 78 | 79 | # 正则语法检测 80 | if ($is_regex) { 81 | eval { qr/$p/ }; 82 | if ($@) { 83 | die "[ERROR] 无效的正则表达式 '$p': $@\n"; 84 | } 85 | } 86 | 87 | return ($is_regex, $p, $r); 88 | } 89 | 90 | sub convert_file { 91 | my ($file, $rules_ref) = @_; 92 | 93 | # 备份处理 94 | my $bak_file = "$file.bak"; 95 | if ($opt{backup} && !-e $bak_file) { 96 | move ($file, $bak_file) 97 | or die "[ERROR] 备份失败: 无法将 '$file' 备份为 '$bak_file': $!\n"; 98 | } 99 | 100 | my $input_file = ($opt{backup} && -e $bak_file) ? $bak_file : $file; 101 | open(my $in, '<:encoding('.$opt{encoding}.')', $input_file) 102 | or die "[ERROR] 无法读取文件 '$input_file': $!\n"; 103 | open(my $out, '>:encoding('.$opt{encoding}.')', "$file.tmp") 104 | or die "[ERROR] 无法创建临时文件 '$file.tmp': $!\n"; 105 | 106 | # 逐行处理 107 | my $count = 0; 108 | while (<$in>) { 109 | my $origin = $_; 110 | foreach my $rule (@$rules_ref) { 111 | s/$rule->{pattern}/$rule->{replace}/g; 112 | } 113 | print $out $_; 114 | $count += 1; 115 | print "已处理 $count 行\r" if $opt{verbose} && $count % 100 == 0; 116 | } 117 | 118 | close $in; 119 | close $out; 120 | 121 | # 文件输出处理 122 | my $out_path; 123 | if (defined $opt{output}) { 124 | if (-d $opt{output}) { 125 | my $basename = $file; 126 | $basename =~ s|.*/||; 127 | $out_path = "$opt{output}/$basename"; 128 | } else { 129 | $out_path = $opt{output}; 130 | } 131 | } else { 132 | $out_path = $file; 133 | } 134 | 135 | move("$file.tmp", $out_path) 136 | or die "[ERROR] 无法将临时文件 '$file.tmp' 移动为 '$out_path': $!\n"; 137 | 138 | unlink $bak_file unless $opt{backup}; 139 | 140 | print "\n[OK] 已转换 $file (共 $count 行)\n" if $opt{verbose}; 141 | print "[OK] 已输出到 $out_path\n" if $opt{verbose}; 142 | } 143 | 144 | sub usage { 145 | print <<"END_USAGE"; 146 | 简繁转换脚本 v2.0 147 | 用法: $0 [选项] 文件1 文件2... 148 | 149 | 选项: 150 | -r, --rules=FILE 规则配置文件 (默认: $opt{rules}) 151 | -b, --backup 保留原始文件备份 (.bak) 152 | -e, --encoding=ENC 文件编码 (默认: $opt{encoding}) 153 | -o, --output=PATH 输出文件路径或目录 154 | -v, --verbose 显示详细处理信息 155 | -h, --help 显示此帮助信息 156 | 157 | 规则文件格式: 158 | 普通替换: 原文本[2+空格]替换文本 159 | 正则替换: REGEX:模式[2+空格]替换文本 160 | 161 | 示例: 162 | $0 -v -b locals_zh-TW.js 163 | $0 -v -o ../locals_zh-TW.js 164 | $0 -r custom_rules.conf *.js 165 | END_USAGE 166 | exit($_[0] || 0); 167 | ; 168 | } 169 | -------------------------------------------------------------------------------- /script/ci_commit_with_signature.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while getopts ":T:R:B:P:F:D:h:b:" opt; do 4 | case $opt in 5 | T) 6 | # 通过 GitHub GraphQL API 进行身份验证的 TOKEN 7 | # TOKEN for authentication via the GitHub GraphQL API 8 | TOKEN=$OPTARG 9 | ;; 10 | R) 11 | # GitHub GraphQL API 请求带有所有者的远程仓库名称 12 | # Remote repository name with owner requested by the GitHub GraphQL API 13 | repoNwo=$OPTARG 14 | ;; 15 | B) 16 | # GitHub GraphQL API 请求的远程仓库目标分支名称 17 | # The name of the target branch of the remote repository requested by the GitHub GraphQL API 18 | branch=$OPTARG 19 | ;; 20 | P) 21 | # 远程仓库目标分支上最后一次提交的 SHA。 22 | # 它也是即将创建的提交的父提交的 SHA。 23 | # The SHA of the last commit on the target branch of the remote repository. 24 | # It is also the SHA of the parent commit of the commit about to be created. 25 | parentSHA=$OPTARG 26 | ;; 27 | F) 28 | # 通过 GitHub GraphQL API 提交, 新增或修改的文件的路径(相对于存储库根)的数组 29 | # Array of paths (relative to the repository root) to new or modified files for commits via the GitHub GraphQL API 30 | IFS=', ' read -ra changed_files <<< "${OPTARG:-}" 31 | # 使用逗号和或空格作为分隔符,将参数分割为数组,默认值为空字符串 32 | # Split parameters into arrays using commas and or spaces as separators, defaults to empty string 33 | ;; 34 | D) 35 | # 通过 GitHub GraphQL API 提交, 删除的文件的路径(相对于存储库根)的数组 36 | # Array of paths (relative to the repository root) to deleted files for commits via the GitHub GraphQL API 37 | IFS=', ' read -ra deleted_files <<< "${OPTARG:-}" 38 | ;; 39 | h) 40 | # 通过 GitHub GraphQL API 提交的提交消息标题行 41 | # Commit message head line committed via GitHub GraphQL API 42 | message_headline=$OPTARG 43 | ;; 44 | b) 45 | # 通过 GitHub GraphQL API 提交的提交消息正文 46 | # Commit message body committed via GitHub GraphQL API 47 | message_body=$OPTARG 48 | ;; 49 | \?) 50 | echo "无效的选项: -$OPTARG" >&2 51 | exit 1 52 | ;; 53 | esac 54 | done 55 | 56 | if [[ -z $TOKEN ]]; then 57 | TOKEN=$GITHUB_TOKEN 58 | fi 59 | 60 | if [[ -z $GITHUB_API_URL ]]; then 61 | GITHUB_API_URL="https://api.github.com" 62 | fi 63 | 64 | function set_dco_signature { 65 | if [[ $TOKEN == ghp_* ]]; then 66 | # https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/ 67 | # 'ghp_'开头的是 GitHub 个人访问令牌 68 | # What starts with 'ghp_' is the GitHub personal access token 69 | 70 | res=$(curl -s -H "Authorization: token $TOKEN" "$GITHUB_API_URL/user" 2>/dev/null || echo '{"login":"gh-actions","id":0}') 71 | else 72 | bot="${APP_SLUG:-github-actions}[bot]" 73 | res=$(curl -sg -H "Authorization: token $TOKEN" "$GITHUB_API_URL/users/${bot}" 2>/dev/null || echo '{"login":"gh-actions","id":0}') 74 | fi 75 | 76 | login=$(jq -r .login <<< "$res") 77 | name=$(jq -r '.name // empty' <<< "$res") 78 | id=$(jq -r .id <<< "$res") 79 | echo "Signed-off-by: ${name:-$login} <$id+$login@users.noreply.github.com>" 80 | } 81 | 82 | message_body="${message_body:+$message_body\n}$(set_dco_signature)" 83 | 84 | # 处理文件修改并构建 fileChanges 部分中 additions 的 JSON 字符串 85 | # Process the file changes and build the JSON string of `additions` in the `fileChanges` section 86 | changed_files_json="" 87 | for file_path in "${changed_files[@]}"; do 88 | changed_files_json+="{ 89 | \"path\": \"$file_path\", 90 | \"contents\": \"$(base64 < "$file_path")\" 91 | }, 92 | " 93 | done 94 | changed_files_json="${changed_files_json%, 95 | }" # 移除最后一个逗号及换行符和空格 96 | # Remove last comma and line breaks and spaces 97 | 98 | # 处理文件删除并构建 fileChanges 部分中 deletions 的 JSON 字符串 99 | # Process the file deletions and build the JSON string of `deletions` in the `fileChanges` section 100 | deleted_files_json="" 101 | for file_path in "${deleted_files[@]}"; do 102 | deleted_files_json+="{ 103 | \"path\": \"$file_path\", 104 | }, 105 | " 106 | done 107 | deleted_files_json="${deleted_files_json%, 108 | }" # 移除最后一个逗号及换行符和空格 109 | # Remove last comma and line breaks and spaces 110 | 111 | # 构建 GraphQL 请求的 JSON 字符串 112 | # Construct JSON string for GraphQL request 113 | graphql_request='{ 114 | "query": "mutation ($input: CreateCommitOnBranchInput!) { 115 | createCommitOnBranch(input: $input) { 116 | commit { 117 | oid, 118 | url 119 | } 120 | } 121 | }", 122 | "variables": { 123 | "input": { 124 | "branch": { 125 | "repositoryNameWithOwner": "'"$repoNwo"'", 126 | "branchName": "'"$branch"'" 127 | }, 128 | "message": { 129 | "headline": "'"$message_headline"'", 130 | "body": "'"$message_body"'" 131 | }, 132 | "fileChanges": { 133 | "additions": [ 134 | '"$changed_files_json"' 135 | ], 136 | "deletions": [ 137 | '"$deleted_files_json"' 138 | ] 139 | }, 140 | "expectedHeadOid": "'"$parentSHA"'" 141 | } 142 | } 143 | }' 144 | 145 | # 将请求数据写入 request.json 文件 146 | # Write the request data to the `request.json` file 147 | echo "$graphql_request" > request.json 148 | 149 | # 发送 GraphQL 请求并解析结果 150 | # Send GraphQL requests and parse the results 151 | if [[ -z $GITHUB_GRAPHQL_URL ]]; then 152 | GITHUB_GRAPHQL_URL="https://api.github.com/graphql" 153 | fi 154 | 155 | response=$(curl "$GITHUB_GRAPHQL_URL" --silent \ 156 | --write-out '%{stderr}HTTP status: %{response_code}\n\n' \ 157 | -H "Authorization: bearer $TOKEN" \ 158 | --data @request.json) 159 | 160 | # Print the results 161 | jq -r ' 162 | if .data?.createCommitOnBranch?.commit?.url then 163 | "✅ 请求成功,SHA: \(.data.createCommitOnBranch.commit.oid)\nURL: \(.data.createCommitOnBranch.commit.url)" 164 | else 165 | if .errors then 166 | "❌ 错误列表:\n" + ([.errors[].message] | join("\n- ")) 167 | else 168 | "⚠️ 未知响应格式: \(.)" 169 | end 170 | end' <<< "$response" 171 | -------------------------------------------------------------------------------- /.github/workflows/main.user.js_version_update_and_sync_zh-TW.yaml: -------------------------------------------------------------------------------- 1 | name: main.user.js version update & sync zh-TW 2 | on: 3 | schedule: 4 | # 每周日的UTC时间16点(北京时间周一凌晨0点) 5 | - cron: "0 16 * * 0" 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | update_main: 13 | runs-on: ubuntu-latest 14 | environment: github-pages 15 | outputs: 16 | locals_js_date: ${{ steps.version_update.outputs.locals_js_date }} 17 | oid: ${{ steps.api_commit.outputs.oid }} 18 | steps: 19 | - name: Generate a token 20 | id: generate_token 21 | uses: actions/create-github-app-token@v2 22 | with: 23 | app-id: ${{ vars.APP_ID }} 24 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 25 | 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: Test if locals.js has changed since the last release 32 | id: detect_locals_js_changes 33 | run: | 34 | # 最近提交是否存在tag 35 | if [ ! "$(git tag --contains ${{ github.sha }})" ]; then 36 | # 获得最近以`main.user.js Update to`开头的提交的sha和时间戳 37 | release_commit_sha=$(git log --grep="^main.user.js Update to" -n 1 --pretty=format:"%H") 38 | release_commit_timestamp=$(git show -s --format=%ct $release_commit_sha) 39 | 40 | # 最近的tag对应提交的sha和时间戳 41 | latest_tag_commit_sha=$(git rev-list --tags --max-count=1) 42 | latest_tag_commit_timestamp=$(git show -s --format=%ct $latest_tag_commit_sha) 43 | 44 | if [ $release_commit_timestamp -gt $latest_tag_commit_timestamp ]; then 45 | ref=$release_commit_sha 46 | else 47 | ref=$latest_tag_commit_sha 48 | fi 49 | 50 | # 如果 locals.js 变化,设置环境变量 51 | git diff --quiet --exit-code $ref locals.js || \ 52 | echo "LOCALS_JS_CHANGED=true" >> $GITHUB_ENV 53 | fi 54 | 55 | # 如果 locals.js 没有变化,直接跳过后续步骤 56 | - name: Exit if no changes in locals.js 57 | if: env.LOCALS_JS_CHANGED != 'true' 58 | run: echo "No changes in locals.js, exiting..." 59 | 60 | - name: Update version in main.user.js 61 | id: version_update 62 | if: env.LOCALS_JS_CHANGED == 'true' 63 | run: | 64 | # 获取最后一次提交中`locals.js`的文件时间 65 | locals_js_time=$(git log -1 --format=%at -- locals.js) 66 | 67 | # 使用`locals.js`的文件日期为词库版本号 68 | locals_js_date=$(TZ='Asia/Shanghai' date -d "@$locals_js_time" +'%Y-%m-%d') 69 | echo "locals_js_date=$locals_js_date" >> $GITHUB_OUTPUT 70 | 71 | # 更新 main.user.js 72 | sed -i -E " 73 | s/(@version\s*([0-9]+\.){2}[0-9]+)(-[0-9]{4}-[0-9]{2}-[0-9]{2})?/\1-$locals_js_date/ 74 | s/(@require.+locals\.js\?v([0-9]+\.){2}[0-9]+)(-[0-9]{4}-[0-9]{2}-[0-9]{2})?/\1-$locals_js_date/ 75 | " main.user.js 76 | 77 | # 如果 main.user.js 变化,设置环境变量 78 | git diff --quiet --exit-code main.user.js || \ 79 | echo "MAIN_USER_JS_CHANGED=true" >> $GITHUB_ENV 80 | 81 | - name: Commit and push main.user.js 82 | id: api_commit 83 | if: env.LOCALS_JS_CHANGED == 'true' && env.MAIN_USER_JS_CHANGED == 'true' 84 | env: 85 | GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 86 | APP_SLUG: ${{ steps.generate_token.outputs.app-slug }} 87 | run: | 88 | version=$(sed -n -E 's/^\/\/ @version\s+(.+)/\1/p' main.user.js) 89 | oid=$(bash script/ci_commit_with_signature.sh \ 90 | -R "${{ github.repository }}" \ 91 | -B "${{ github.ref_name }}" \ 92 | -P "${{ github.sha }}" \ 93 | -F "main.user.js" \ 94 | -h "main.user.js Update to $version" | tee >(cat >&2) | grep -oP '✅ 请求成功,SHA: \K[a-f0-9]{40}') 95 | 96 | # 验证SHA格式 97 | if [[ "$oid" =~ ^[a-f0-9]{40}$ ]]; then 98 | echo "oid=$oid" >> $GITHUB_OUTPUT 99 | fi 100 | 101 | sync_zh_tw: 102 | runs-on: ubuntu-latest 103 | if: success() && needs.update_main.outputs.oid 104 | needs: update_main 105 | environment: github-pages 106 | steps: 107 | - name: Checkout files 108 | uses: actions/checkout@v4 109 | with: 110 | ref: ${{ needs.update_main.outputs.oid }} 111 | 112 | - name: Generate token 113 | id: generate_token 114 | uses: actions/create-github-app-token@v2 115 | with: 116 | app-id: ${{ vars.APP_ID }} 117 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 118 | 119 | - name: Cache apt packages 120 | uses: actions/cache@v4 121 | with: 122 | path: | 123 | /var/cache/apt/archives/*.deb 124 | !/var/cache/apt/archives/partial 125 | !/var/cache/apt/archives/lock 126 | key: apt-${{ runner.os }}-${{ hashFiles('**/apt-packages.txt') }} 127 | restore-keys: | 128 | apt-${{ runner.os }}- 129 | 130 | - name: Install OpenCC 131 | run: | 132 | sudo apt-get update 133 | sudo apt-get install -y opencc 134 | 135 | - name: Sync locals.js -> locals_zh-TW.js 136 | run: | 137 | opencc -i locals.js -o locals_zh-TW.js -c s2tw.json 138 | perl ./script/t2s-convert.pl -v locals_zh-TW.js 139 | 140 | - name: Update version in main_zh-TW.user.js 141 | run: | 142 | # 同步版本号 143 | locals_js_date="${{ needs.update_main.outputs.locals_js_date }}" 144 | sed -i -E " 145 | s/(@version\s*([0-9]+\.){2}[0-9]+)(-[0-9]{4}-[0-9]{2}-[0-9]{2})?/\1-$locals_js_date/ 146 | s/(@require.+locals_zh-TW.js\?v([0-9]+\.){2}[0-9]+)(-[0-9]{4}-[0-9]{2}-[0-9]{2})?/\1-$locals_js_date/ 147 | " main_zh-TW.user.js 148 | 149 | - name: Commit and push main_zh-TW.user.js, locals_zh-TW.js 150 | env: 151 | GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 152 | APP_SLUG: ${{ steps.generate_token.outputs.app-slug }} 153 | run: | 154 | version=$(sed -n -E 's/^\/\/ @version\s+(.+)/\1/p' main_zh-TW.user.js) 155 | bash script/ci_commit_with_signature.sh \ 156 | -R "${{ github.repository }}" \ 157 | -B "${{ github.ref_name }}" \ 158 | -P "${{ needs.update_main.outputs.oid }}" \ 159 | -F "main_zh-TW.user.js, locals_zh-TW.js" \ 160 | -h "main_zh-TW.user.js Sync Thesaurus to $version" 161 | -------------------------------------------------------------------------------- /README_zh-TW.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # [GitHub 中文化插件][github-project-link] 4 | 5 | > 讓 GitHub 界面全面中文化 | 源自 [52cik/github-hans](https://github.com/52cik/github-hans) 6 | 7 | [简体中文](./README.md) · **繁體中文** · [反饋問題][github-issues-link] 8 | 9 | 10 | 11 | [![GitHub issues][github-issues-shield]][github-issues-link] 12 | [![GitHub stars][github-stars-shield]][github-stars-link] 13 | [![GitHub forks][github-forks-shield]][github-forks-link] 14 | [![license GPL-3.0][github-license-shield]][github-license-link] 15 | [![GreasyFork installs][greasyFork-shield]][greasyFork-link] 16 | 17 | 18 | 19 | 20 | 21 | Featured|HelloGitHub 22 | 23 | 24 | 25 |
26 | 27 | > [!warning] 28 | > 本項目從未發佈至 GitCode,如您發現請截圖並保留證據 29 | 30 | > [!IMPORTANT] 31 | > 💡 **重要:** 本繁體語言版本,使用基於[OpenCC](https://github.com/BYVoid/OpenCC)及[自定義規則](./t2s_rules.conf)轉換 32 | 33 |
34 | 目錄樹 35 | 36 | #### TOC 37 | - [🌟 功能特性](#-功能特性) 38 | - [🌐 兼容環境](#-兼容環境) 39 | - [💻 安裝指南](#-安裝指南) 40 | - [🔧 本地調試](#-本地調試) 41 | - [🔄 更新日誌](#-更新日誌) 42 | - [📌 待辦事項](#-待辦事項) 43 | - [🤝 參與貢獻](#-參與貢獻) 44 | - [🖼️ 效果預覽](#-效果預覽) 45 | - [🙏 特別鳴謝](#-特別鳴謝) 46 | - [📈 項目統計](#-項目統計) 47 | - [🎁 歡迎打賞](#-歡迎打賞) 48 |
49 | 50 | ## 🌟 功能特性 51 | 52 | - [x] 全面中文化 GitHub 界面元素(菜單欄、標題、按鈕等) 53 | - [x] 智能正則匹配功能 54 | - [x] 支持項目描述的人機翻譯 55 | - [x] 自動本地化時間元素 56 | - [x] 持續更新詞庫 57 | 58 | ## 🌐 兼容環境 59 | 60 | 瀏覽器類型 | 支持的腳本管理器 61 | :------------------: | :---------------: 62 | Chrome / Chromium 內核 | [Tampermonkey][Tampermonkey], [Violentmonkey][Violentmonkey] 63 | Safari(全平臺) | [Macaque][Macaque], [Stay][Stay] 64 | Firefox / Gecko 內核 | [Tampermonkey][Tampermonkey], [Violentmonkey][Violentmonkey] 65 | Via(Android) | 內置管理器 66 | 67 | ## 💻 安裝指南 68 | 69 | 1. 安裝用戶腳本管理器: 70 | - 推薦:[Tampermonkey][Tampermonkey] 71 | 1. **基於 Chrome / Chromium 內核瀏覽器:** 72 | 1. 務必開啟 「擴展程序」 管理中的 **「開發者模式」**[^1] 73 | 1. 務必開啟 「擴展程序」 管理中腳本管理器擴展的 **「允許運行用戶腳本」** 74 | 1. 具體可參考 [Tampermonkey 官方指引](https://www.tampermonkey.net/faq.php#Q209) 75 | 1. 選擇安裝源: 76 | - [GitHub 源【開發版】][main_zh-TW.user.js] 77 | 1. 刷新頁面後,插件即可生效 78 | 1. 必要時,重啟瀏覽器 79 | 80 | [^1]: [Chrome 切換到 Manifest V3後,使用問題](https://github.com/maboloshi/github-chinese/issues/234) 81 | 82 | > [!NOTE] 83 | > **版本說明**: 84 | > - 🚀 開發版:實時更新,每週五自動更新詞庫 85 | > - 🛡️ 穩定版:每週一同步開發版詞庫,更穩定 86 | 87 | ## 🔧 本地調試 88 | 89 | 1. 安裝 [Tampermonkey][Tampermonkey],並啟用 “允許訪問文件網址”。 90 | 1. 下載詞庫文件到本地(如:`D:\github-chinese\locals.js`) 91 | 1. 在腳本管理器中修改引用路徑: 92 | ```js 93 | // 原始路徑 94 | // @require https://raw.githubusercontent.com/... 95 | 96 | // 修改為 97 | // @require file:///D:/github-chinese/locals.js 98 | ``` 99 | 1. 刷新頁面生效 100 | 101 | > [!TIP] 102 | > 💡 **溫馨提示:** 您可以將詞庫文件拖拽至瀏覽器地址欄,複製路徑直接使用。 103 | 104 |
105 | 106 | [![][back-to-top]](#readme-top) 107 | 108 |
109 | 110 | 111 | ## 🔄 更新日誌 112 | 113 | ### 最新版本 114 | 115 | #### v1.9.3 (2024-08-18) 116 | 117 | 1. 新增功能:通過設置中文環境,自動本地化時間元素,僅保留`on`開頭的時間正則,並停用時間元素監視 118 | 1. 優化突變翻譯處理: 119 | - 引入`characterDataPage`規則,對特定頁面啟用`篩選字符數據`的變更 120 | - 引入`ignoreMutationSelectorPage`規則,忽略特定突變元素 121 | 1. 合併`reIgnoreClass,reIgnoreItemprop,ignoreId,ignoreTag`為`ignoreSelectorPage`規則,處理全局及特定頁面,忽略特定元素 122 | 1. 引入全局緩存模式,減少重複構建包括不限於基於`page`變化的忽略規則、正則規則數組等 123 | 1. 調整:更新訊飛聽見翻譯引擎v2.0 124 | 1. 優化:梳理、優化腳本 125 | 1. 調整:調整詞庫語言代碼為`zh-CN`, 與環境語言設置一致 126 | 127 |
查看更多歷史版本 128 | 129 | #### v1.9.2 (2024-06-14) 130 | 131 | 1. 適配`www.githubstatus.com` 132 | 1. 適配`skills.github.com` 133 | 134 | #### v1.9.1 (2024-05-23) 135 | 136 | 1. 更新`切換正則功能按鈕` 137 | 138 | #### v1.9.0 (2023-12-09) 139 | 140 | 1. 重新定義版本號規則, 如`1.9.0-2023-12-09`。 141 | - `1.9.0`: 主版本號(由項目所有者更新) 142 | - `2023-12-09`:`詞庫`發佈版本號(由 GitHub Action 自動更新) 143 | 1. 加強: [GitHub 源【開發版】][main.user.js]每週一凌晨自動更新`詞庫`發佈版本號 144 | 1. 加強: [GreasyFork 源【穩定版】][main(greasyfork).user.js]每週五凌晨自動更新`詞庫`發佈版本號, 詞庫內容同上一次[GitHub 源【開發版】][main.user.js] 145 | 1. 加強:在 `README.md` 中自動更新貢獻者頭像 146 | 1. 更新: 忽略規則, 詞條等 147 | 148 | #### v1.8.5 (2023-08-31) 149 | 150 | 1. 優化: `transDesc 函數`代碼 151 | 1. 修復: 重複添加`translate-me`翻譯按鈕 152 | 1. 加強:`watchUpdate 函數`新增節點文本更新的情況 153 | 1. 調整: `transBySelector和transDesc函數`延遲執行時間 154 | 1. 更新: 忽略規則, 詞條等 155 | 156 | #### v1.8.4 (2023-08-08) 157 | 158 | 1. 修復: `Itemprop`過濾規則, 依然使用正則方式 159 | 1. 修復: `tooltipped`樣式提示, 依然使用正則方式 160 | 161 | #### v1.8.3 (2023-08-07) 162 | 163 | 1. 梳理、優化腳本 164 | 1. 更新: 忽略規則, 大量詞條等 165 | 166 | #### v1.8.2 (2023-05-15) 167 | 168 | 1. `greasyfork 託管`源切換到`按頁面精細化詞條模式` 169 | 1. 調整詞庫格式 170 | 1. 功能加強: 優化`元素篩選器`翻譯邏輯 171 | 1. 更新: 忽略規則, 大量詞條等 172 | 173 | #### v1.8.1 (2023-01-22) 174 | 175 | 1. 修復: #8 與 dark reader 擴展發生衝突,導致時間顯示出現問題 176 | 1. `GitHub`源開始切換到`按頁面精細化詞條模式(開發版)`, 詞庫未完全遷移適配 177 | 1. 停止`greasyfork`源詞庫文件的同步更新 178 | 179 | #### v1.8.0 (2023-01-18) 180 | 181 | 1. 刪除: `TURBO-FRAME`框架處理代碼. Github 已調整新動態加載模式, 直接檢測`url`的變化就能獲取對應的`page`信息 182 | 1. 新增: 支持時間元素的`Shadow DOM`翻譯, 並監聽變化 183 | 1. 新增: 啟用並更新`時間元素翻譯`專項正則詞條 184 | 1. 新增: 僅當`page`有效才翻譯頁面 185 | 1. 修復: 原`簡介翻譯`引擎`GitHub中文社區`失效, 改為`訊飛`引擎(測試) 186 | 1. 修復: 追加公共正則重複迭代的問題 187 | 1. 修復: 正則標記變量`RegExp`與構造函數`new RegExp`衝突 188 | 1. 更新: 忽略規則, 詞條等 189 | 190 | 預告, 下次將細化`page`匹配規則, 導致詞庫文件結構大調整, 詞庫文件會適當變大, 頁面正則更精細效率會提升 191 | 192 | #### v1.7.9 (2022-07-17) 193 | 194 | GitHub 的 ajax 載入方式逐步從 [defunkt/jquery-pjax](https://github.com/defunkt/jquery-pjax) 切換到 [hotwired/turbo](turbo.hotwired.dev), 導致已有的動態監測方式逐步失效 195 | 196 | 目前, 通過以下修復: 197 | 198 | 1. 新增 `BODY` 元素新增監視 199 | 1. 解析 `TURBO-FRAME` 框架, 獲取對應的 `page` 200 | 1. 修復 github 新動態加載模式, 導致`翻譯描述`返回值無法插入 201 | 1. 修復 github 新動態加載模式, 導致`chrome`瀏覽器自帶翻譯功能卡死頁面 202 | 203 | 其他更新: 204 | 205 | 1. 修復`rePagePath`,`rePagePathRepo`,`rePagePathOrg`匹配規則,限制路徑匹配層次,排除干擾 206 | 1. 直接使用網頁URL`document.URL`變化觸發`標題翻譯`和`JS 篩選器`翻譯 207 | 1. 修復`關閉正則`無法生效, 需要刷新頁面才生效 208 | 1. 日常更新詞庫和忽略規則 209 | 1. 更新`JS 篩選器`規則 210 | 211 | #### v1.7.8 (2022-06-29) 212 | 213 | 1. 緊急修復: GitHub 變更了`document.body`和`title`更新機制, 導致原有的`監測更新`規則部分失效, 目前使用`document.documentElement`監視整個頁面 DOM 的變更 214 | 1. 跳過``標籤 215 | 1. `標題翻譯`和`JS 篩選器`翻譯, 依據 URL變化更新 216 | 217 | #### v1.7.7 (2022-06-26) 218 | 219 | 1. 新增`時間元素翻譯`功能 220 | 1. 重寫`頁面標題翻譯`函數 221 | 1. 梳理`遍歷節點`函數邏輯 222 | 1. 優化`transPage`函數,默認翻譯公共部分 223 | 1. 調整`getPage`函數, 使`ClassName匹配規則`優先 224 | 1. 優化`translate`函數, 跳過`不存在英文字母和符號,.`, 保留首尾空白部分等 225 | 1. 部分函數重命名,使用`es6`新語法 226 | 1. 日常更新詞庫和忽略規則,修復一個`JS 選擇器規則` 227 | 228 | #### v1.7.6 (2022-05-12) 229 | 230 | 1. 日常更新詞庫和忽略規則 231 | 1. 添加手動開啟/禁用正則翻譯,添加切換菜單 232 | 1. 優化翻譯文本函數:避免已翻譯詞彙二次匹配,提高效率;局部翻譯優先於全局 233 | 234 |
235 | 236 |
237 | 238 | [![][back-to-top]](#readme-top) 239 | 240 |
241 | 242 | 243 | ## 📌 待辦事項 244 | 245 | 1. 添加 GitHub 專用名詞解釋 246 | 1. 整理 [Git](https://git-scm.com/) & [GitHub](https://github.com/) 學習資料 247 | 1. 完善文檔翻譯,需大家 PR 共同翻譯 248 | 249 | ## 🤝 參與貢獻 250 | 251 | 歡迎通過以下方式參與貢獻: 252 | 253 | 1. 完善詞庫翻譯(編輯 `locals.js`) 254 | 1. 提交議題報告,參與話題討論 255 | 1. 改進代碼邏輯 256 | 257 | [![][pr-welcome-shield]][pr-welcome-link] 258 | 259 | ### 翻譯參考資源: 260 | 261 | 1. [Pro Git 第二版 簡體中文](https://git-scm.com/book/zh/v2) 262 | 1. [Pro Git: 翻譯約定](https://github.com/progit/progit2-zh/blob/master/TRANSLATION_NOTES.asc) 263 | 1. [Git 官方軟件包的簡體中文翻譯](https://github.com/git/git/blob/master/po/zh_CN.po) 264 | 1. [GitHub 詞彙表官方譯本](https://docs.github.com/cn/get-started/quickstart/github-glossary) 265 | 1. **[CSS 選擇器](https://developer.mozilla.org/zh-CN/docs/Web/CSS/Reference/Selectors) 用於編寫忽略規則** 266 | 267 | > [查看詳細貢獻指南](https://github.com/maboloshi/github-chinese/discussions/57) 268 | 269 | ## 🖼️ 效果預覽 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | ## 🙏 特別鳴謝 283 | 284 | ### 核心團隊 285 | 286 | - [maboloshi](https://github.com/maboloshi) - 項目作者 287 | - [wyc-26](https://github.com/wyc-26),[陳生雜物房](https://github.com/TC999) - 項目協作者 288 | - [52cik](https://github.com/52cik) - 項目原作者 289 | 290 | ### 貢獻者牆 291 | 292 | 一如既往,感謝我們出色的貢獻者❤️! 293 | 294 | 295 | 296 | 沙漠之子 297 | 298 | 299 | 樓教主 300 | 301 | 302 | 陳生雜物房 303 | 304 | 305 | 其智乃反不能及 306 | 307 | 308 | wyc-26 309 | 310 | 311 | 大葉子 312 | 313 | 314 | cat-kun 315 | 316 | 317 | 人民的勤務員 318 | 319 | 320 | 菾凴 321 | 322 | 323 | 𠭞 324 | 325 | 326 | 益生君 327 | 328 | 329 | 小莫 330 | 331 | 332 | 前端小武 333 | 334 | 335 | wang4yu6peng13 336 | 337 | 338 | pangshitong 339 | 340 | 341 | dayday 342 | 343 | 344 | create new ██████╗  ██╔══██╗ ██████╔╝ ██╔══██╗ ██████╔╝ ╚═════╝    ██╗    ██╗   ██║    ██║   ██║    ██║   ██║    ██║   ╚█████╔╝    ╚═════╝  ███████╗ ██╔════╝  ██║████═╗  ██║    ██ ║ ╚██████╔╝   ╚══════╝ 345 | 346 | 347 | KS-OTO 348 | 349 | 350 | InfinityLoop 351 | 352 | 353 | Imgbot 354 | 355 | 356 | neveler 357 | 358 | 359 | 360 | > 貢獻者列表,由 [GitHub Action][update-contributors-images] 自動生成 361 | 362 |
363 | 364 | [![][back-to-top]](#readme-top) 365 | 366 |
367 | 368 | 369 | ## 📈 項目統計 370 | 371 | 372 | 373 | 374 | 375 | Star History Chart 376 | 377 | 378 | 379 | ![Alt](https://repobeats.axiom.co/api/embed/ae4c378f0e6ec317654ec5c4e8b01218c734cd53.svg "Repobeats analytics image") 380 | 381 |
382 | 383 | [![][back-to-top]](#readme-top) 384 | 385 |
386 | 387 | ## 🎁 歡迎打賞 388 | [讚賞列表](https://github.com/maboloshi/maboloshi/issues/1) 389 | | 微信讚賞 | 支付寶讚賞 | 390 | | :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: | 391 | | WeChat QRcode
☕喝點咖啡繼續幹☕ | AliPay QRcode
🌶️來包辣條吧~🍪 | 392 | 393 | 394 | 395 | 396 | [back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square 397 | [github-project-link]: https://github.com/maboloshi/github-chinese "GitHub 中文化插件" 398 | [github-issues-link]: https://github.com/maboloshi/github-chinese/issues "議題" 399 | [github-issues-shield]: https://img.shields.io/github/issues/maboloshi/github-chinese?style=flat-square&logo=github&label=Issue 400 | [github-stars-link]: https://github.com/maboloshi/github-chinese/stargazers "星標" 401 | [github-stars-shield]: https://img.shields.io/github/stars/maboloshi/github-chinese?style=flat-square&logo=github&label=Star 402 | [github-forks-link]: https://github.com/maboloshi/github-chinese/network "復刻" 403 | [github-forks-shield]: https://img.shields.io/github/forks/maboloshi/github-chinese?style=flat-square&logo=github&label=Fork 404 | [github-license-link]: https://opensource.org/licenses/GPL-3.0 "許可證" 405 | [github-license-shield]: https://img.shields.io/github/license/maboloshi/github-chinese?style=flat-square&logo=github&label=License 406 | [greasyFork-link]: https://greasyfork.org/scripts/435208 "GreasyFork 源 - GitHub 中文化插件" 407 | [greasyFork-shield]: https://img.shields.io/badge/dynamic/json?style=flat-square&logo=GreasyFork&label=GreasyFork&query=total_installs&suffix=%20installs&url=https://greasyfork.org/scripts/435208.json 408 | [pr-welcome-link]: https://github.com/maboloshi/github-chinese/pulls 409 | [pr-welcome-shield]: https://img.shields.io/badge/🤯_pr_welcome-%E2%86%92-ffcb47?labelColor=black&style=for-the-badge "歡迎提交 PR" 410 | [Tampermonkey]: http://tampermonkey.net/ "篡改猴" 411 | [Violentmonkey]: https://violentmonkey.github.io/ "暴力猴" 412 | [Macaque]: https://macaque.app/ "獼猴" 413 | [Stay]: https://apps.apple.com/cn/app/stay-for-safari-%E6%B5%8F%E8%A7%88%E5%99%A8%E4%BC%B4%E4%BE%A3/id1591620171 "Stay" 414 | [main_zh-TW.user.js]: https://github.com/maboloshi/github-chinese/raw/gh-pages/main_zh-TW.user.js "GitHub 中文化插件(繁體版) - GitHub 託管" 415 | [update-contributors-images]: https://github.com/maboloshi/github-chinese/blob/gh-pages/.github/workflows/update_contributors_images.yml 416 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # [GitHub 中文化插件][github-project-link] 4 | 5 | > 让 GitHub 界面全面中文化 | 源自 [52cik/github-hans](https://github.com/52cik/github-hans) 6 | 7 | **简体中文** · [繁體中文](./README_zh-TW.md) · [反馈问题][github-issues-link] 8 | 9 | 10 | 11 | [![GitHub stars][github-stars-shield]][github-stars-link] 12 | [![GitHub forks][github-forks-shield]][github-forks-link] 13 | [![GitHub issues][github-issues-shield]][github-issues-link] 14 | [![license GPL-3.0][github-license-shield]][github-license-link] 15 | [![GreasyFork installs][greasyFork-shield]][greasyFork-link] 16 | 17 | 18 | 19 | 20 | 21 | Featured|HelloGitHub 22 | 23 | 24 | 25 |
26 | 27 | > [!warning] 28 | > 本项目从未发布至 GitCode,如您发现请截图并保留证据 29 | 30 |
31 | 目录树 32 | 33 | #### TOC 34 | - [🌟 功能特性](#-功能特性) 35 | - [🌐 兼容环境](#-兼容环境) 36 | - [💻 安装指南](#-安装指南) 37 | - [🔧 本地调试](#-本地调试) 38 | - [🔄 更新日志](#-更新日志) 39 | - [📌 待办事项](#-待办事项) 40 | - [🤝 参与贡献](#-参与贡献) 41 | - [🖼️ 效果预览](#-效果预览) 42 | - [🙏 特别鸣谢](#-特别鸣谢) 43 | - [📈 项目统计](#-项目统计) 44 | - [🎁 欢迎打赏](#-欢迎打赏) 45 |
46 | 47 | ## 🌟 功能特性 48 | 49 | - [x] 全面中文化 GitHub 界面元素(菜单栏、标题、按钮等) 50 | - [x] 智能正则匹配功能 51 | - [x] 支持项目描述的人机翻译 52 | - [x] 自动本地化时间元素 53 | - [x] 持续更新词库 54 | 55 | ## 🌐 兼容环境 56 | 57 | 浏览器类型 | 支持的脚本管理器 58 | :------------------: | :---------------: 59 | Chrome / Chromium 内核 | [Tampermonkey][Tampermonkey], [Violentmonkey][Violentmonkey] 60 | Safari(全平台) | [Tampermonkey][Tampermonkey], [Macaque][Macaque], [Stay][Stay] 61 | Firefox / Gecko 内核 | [Tampermonkey][Tampermonkey], [Violentmonkey][Violentmonkey] 62 | Via(Android) | 内置管理器 63 | 64 | ## 💻 安装指南 65 | 66 | 1. 安装用户脚本管理器: 67 | - 推荐:[Tampermonkey][Tampermonkey] 68 | 1. **基于 Chrome / Chromium 内核浏览器:** 69 | 1. 务必开启 “扩展程序” 管理中的 **“开发者模式”**[^1] 70 | 1. 务必开启 “扩展程序” 管理中脚本管理器扩展的 **“允许运行用户脚本”** 71 | 1. 具体可参考 [Tampermonkey 官方指引](https://www.tampermonkey.net/faq.php#Q209) 72 | 1. 选择安装源: 73 | - [GitHub 源【开发版】][main.user.js] 74 | - [GreasyFork 源【稳定版】][main(greasyfork).user.js] 75 | 1. 刷新页面后,插件即可生效 76 | 1. 必要时,重启浏览器 77 | 78 | [^1]: [Chrome 切换到 Manifest V3后,使用问题](https://github.com/maboloshi/github-chinese/issues/234) 79 | 80 | > [!NOTE] 81 | > **版本说明**: 82 | > - 🚀 开发版:实时更新,每周五自动更新词库 83 | > - 🛡️ 稳定版:每周一同步开发版词库,更稳定 84 | 85 | ## 🔧 本地调试 86 | 87 | 1. 安装 [Tampermonkey][Tampermonkey],并启用 “允许访问文件网址”。 88 | 1. 下载词库文件到本地(如:`D:\github-chinese\locals.js`) 89 | 1. 在脚本管理器中修改引用路径: 90 | ```js 91 | // 原始路径 92 | // @require https://raw.githubusercontent.com/... 93 | 94 | // 修改为 95 | // @require file:///D:/github-chinese/locals.js 96 | ``` 97 | 1. 刷新页面生效 98 | 99 | > [!IMPORTANT] 100 | > **若无效:** 101 | > 1. 进入 [Tampermonkey][Tampermonkey] 插件`设置页` 102 | > 1. 将 `通用 - 配置模式` 设置为`高级`,进入高级设置模式 103 | > 1. 找到 `安全 - 允许脚本访问本地文件` 并设置为 `外部(@require 和 @resource)` 104 | 105 | > [!TIP] 106 | > 💡 **温馨提示:** 您可以将词库文件拖拽至浏览器地址栏,复制路径直接使用。 107 | 108 |
109 | 110 | [![][back-to-top]](#readme-top) 111 | 112 |
113 | 114 | 115 | ## 🔄 更新日志 116 | 117 | ### 最新版本 118 | 119 | #### v1.9.3 (2024-08-18) 120 | 121 | 1. 新增功能:通过设置中文环境,自动本地化时间元素,仅保留`on`开头的时间正则,并停用时间元素监视 122 | 1. 优化突变翻译处理: 123 | - 引入`characterDataPage`规则,对特定页面启用`筛选字符数据`的变更 124 | - 引入`ignoreMutationSelectorPage`规则,忽略特定突变元素 125 | 1. 合并`reIgnoreClass,reIgnoreItemprop,ignoreId,ignoreTag`为`ignoreSelectorPage`规则,处理全局及特定页面,忽略特定元素 126 | 1. 引入全局缓存模式,减少重复构建包括不限于基于`page`变化的忽略规则、正则规则数组等 127 | 1. 调整:更新讯飞听见翻译引擎v2.0 128 | 1. 优化:梳理、优化脚本 129 | 1. 调整:调整词库语言代码为`zh-CN`, 与环境语言设置一致 130 | 131 |
查看更多历史版本 132 | 133 | #### v1.9.2 (2024-06-14) 134 | 135 | 1. 适配`www.githubstatus.com` 136 | 1. 适配`skills.github.com` 137 | 138 | #### v1.9.1 (2024-05-23) 139 | 140 | 1. 更新`切换正则功能按钮` 141 | 142 | #### v1.9.0 (2023-12-09) 143 | 144 | 1. 重新定义版本号规则, 如`1.9.0-2023-12-09`。 145 | - `1.9.0`: 主版本号(由项目所有者更新) 146 | - `2023-12-09`:`词库`发布版本号(由 GitHub Action 自动更新) 147 | 1. 加强: [GitHub 源【开发版】][main.user.js]每周一凌晨自动更新`词库`发布版本号 148 | 1. 加强: [GreasyFork 源【稳定版】][main(greasyfork).user.js]每周五凌晨自动更新`词库`发布版本号, 词库内容同上一次[GitHub 源【开发版】][main.user.js] 149 | 1. 加强:在 `README.md` 中自动更新贡献者头像 150 | 1. 更新: 忽略规则, 词条等 151 | 152 | #### v1.8.5 (2023-08-31) 153 | 154 | 1. 优化: `transDesc 函数`代码 155 | 1. 修复: 重复添加`translate-me`翻译按钮 156 | 1. 加强:`watchUpdate 函数`新增节点文本更新的情况 157 | 1. 调整: `transBySelector和transDesc函数`延迟执行时间 158 | 1. 更新: 忽略规则, 词条等 159 | 160 | #### v1.8.4 (2023-08-08) 161 | 162 | 1. 修复: `Itemprop`过滤规则, 依然使用正则方式 163 | 1. 修复: `tooltipped`样式提示, 依然使用正则方式 164 | 165 | #### v1.8.3 (2023-08-07) 166 | 167 | 1. 梳理、优化脚本 168 | 1. 更新: 忽略规则, 大量词条等 169 | 170 | #### v1.8.2 (2023-05-15) 171 | 172 | 1. `greasyfork 托管`源切换到`按页面精细化词条模式` 173 | 1. 调整词库格式 174 | 1. 功能加强: 优化`元素筛选器`翻译逻辑 175 | 1. 更新: 忽略规则, 大量词条等 176 | 177 | #### v1.8.1 (2023-01-22) 178 | 179 | 1. 修复: #8 与 dark reader 扩展发生冲突,导致时间显示出现问题 180 | 1. `GitHub`源开始切换到`按页面精细化词条模式(开发版)`, 词库未完全迁移适配 181 | 1. 停止`greasyfork`源词库文件的同步更新 182 | 183 | #### v1.8.0 (2023-01-18) 184 | 185 | 1. 删除: `TURBO-FRAME`框架处理代码. Github 已调整新动态加载模式, 直接检测`url`的变化就能获取对应的`page`信息 186 | 1. 新增: 支持时间元素的`Shadow DOM`翻译, 并监听变化 187 | 1. 新增: 启用并更新`时间元素翻译`专项正则词条 188 | 1. 新增: 仅当`page`有效才翻译页面 189 | 1. 修复: 原`简介翻译`引擎`GitHub中文社区`失效, 改为`讯飞`引擎(测试) 190 | 1. 修复: 追加公共正则重复迭代的问题 191 | 1. 修复: 正则标记变量`RegExp`与构造函数`new RegExp`冲突 192 | 1. 更新: 忽略规则, 词条等 193 | 194 | 预告, 下次将细化`page`匹配规则, 导致词库文件结构大调整, 词库文件会适当变大, 页面正则更精细效率会提升 195 | 196 | #### v1.7.9 (2022-07-17) 197 | 198 | GitHub 的 ajax 载入方式逐步从 [defunkt/jquery-pjax](https://github.com/defunkt/jquery-pjax) 切换到 [hotwired/turbo](turbo.hotwired.dev), 导致已有的动态监测方式逐步失效 199 | 200 | 目前, 通过以下修复: 201 | 202 | 1. 新增 `BODY` 元素新增监视 203 | 1. 解析 `TURBO-FRAME` 框架, 获取对应的 `page` 204 | 1. 修复 github 新动态加载模式, 导致`翻译描述`返回值无法插入 205 | 1. 修复 github 新动态加载模式, 导致`chrome`浏览器自带翻译功能卡死页面 206 | 207 | 其他更新: 208 | 209 | 1. 修复`rePagePath`,`rePagePathRepo`,`rePagePathOrg`匹配规则,限制路径匹配层次,排除干扰 210 | 1. 直接使用网页URL`document.URL`变化触发`标题翻译`和`JS 筛选器`翻译 211 | 1. 修复`关闭正则`无法生效, 需要刷新页面才生效 212 | 1. 日常更新词库和忽略规则 213 | 1. 更新`JS 筛选器`规则 214 | 215 | #### v1.7.8 (2022-06-29) 216 | 217 | 1. 紧急修复: GitHub 变更了`document.body`和`title`更新机制, 导致原有的`监测更新`规则部分失效, 目前使用`document.documentElement`监视整个页面 DOM 的变更 218 | 1. 跳过``标签 219 | 1. `标题翻译`和`JS 筛选器`翻译, 依据 URL变化更新 220 | 221 | #### v1.7.7 (2022-06-26) 222 | 223 | 1. 新增`时间元素翻译`功能 224 | 1. 重写`页面标题翻译`函数 225 | 1. 梳理`遍历节点`函数逻辑 226 | 1. 优化`transPage`函数,默认翻译公共部分 227 | 1. 调整`getPage`函数, 使`ClassName匹配规则`优先 228 | 1. 优化`translate`函数, 跳过`不存在英文字母和符号,.`, 保留首尾空白部分等 229 | 1. 部分函数重命名,使用`es6`新语法 230 | 1. 日常更新词库和忽略规则,修复一个`JS 选择器规则` 231 | 232 | #### v1.7.6 (2022-05-12) 233 | 234 | 1. 日常更新词库和忽略规则 235 | 1. 添加手动开启/禁用正则翻译,添加切换菜单 236 | 1. 优化翻译文本函数:避免已翻译词汇二次匹配,提高效率;局部翻译优先于全局 237 | 238 |
239 | 240 |
241 | 242 | [![][back-to-top]](#readme-top) 243 | 244 |
245 | 246 | 247 | ## 📌 待办事项 248 | 249 | 1. 添加 GitHub 专用名词解释 250 | 1. 整理 [Git](https://git-scm.com/) & [GitHub](https://github.com/) 学习资料 251 | 1. 完善文档翻译,需大家 PR 共同翻译 252 | 253 | ## 🤝 参与贡献 254 | 255 | 欢迎通过以下方式参与贡献: 256 | 257 | 1. 完善词库翻译(编辑 `locals.js`) 258 | 1. 提交议题报告,参与话题讨论 259 | 1. 改进代码逻辑 260 | 261 | [![][pr-welcome-shield]][pr-welcome-link] 262 | 263 | ### 翻译参考资源: 264 | 265 | 1. [Pro Git 第二版 简体中文](https://git-scm.com/book/zh/v2) 266 | 1. [Pro Git: 翻译约定](https://github.com/progit/progit2-zh/blob/master/TRANSLATION_NOTES.asc) 267 | 1. [Git 官方软件包的简体中文翻译](https://github.com/git/git/blob/master/po/zh_CN.po) 268 | 1. [GitHub 词汇表官方译本](https://docs.github.com/cn/get-started/quickstart/github-glossary) 269 | 1. **[CSS 选择器](https://developer.mozilla.org/zh-CN/docs/Web/CSS/Reference/Selectors) 用于编写忽略规则** 270 | 271 | > [查看详细贡献指南](https://github.com/maboloshi/github-chinese/discussions/57) 272 | 273 | ## 🖼️ 效果预览 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | ## 🙏 特别鸣谢 287 | 288 | ### 核心团队 289 | 290 | - [maboloshi](https://github.com/maboloshi) - 项目作者 291 | - [wyc-26](https://github.com/wyc-26),[陈生杂物房](https://github.com/TC999) - 项目协作者 292 | - [52cik](https://github.com/52cik) - 项目原作者 293 | 294 | ### 贡献者墙 295 | 296 | 一如既往,感谢我们出色的贡献者❤️! 297 | 298 | 299 | 300 | 沙漠之子 301 | 302 | 303 | 楼教主 304 | 305 | 306 | 陈生杂物房 307 | 308 | 309 | 其智乃反不能及 310 | 311 | 312 | wyc-26 313 | 314 | 315 | Paper Moon 316 | 317 | 318 | 大叶子 319 | 320 | 321 | cat-kun 322 | 323 | 324 | 人民的勤务员 325 | 326 | 327 | 菾凴 328 | 329 | 330 | Pecasha 331 | 332 | 333 | 苓𥤚 334 | 335 | 336 | 益生君 337 | 338 | 339 | 小莫 340 | 341 | 342 | 前端小武 343 | 344 | 345 | wang4yu6peng13 346 | 347 | 348 | pangshitong 349 | 350 | 351 | dayday 352 | 353 | 354 | create new ██████╗  ██╔══██╗ ██████╔╝ ██╔══██╗ ██████╔╝ ╚═════╝    ██╗    ██╗   ██║    ██║   ██║    ██║   ██║    ██║   ╚█████╔╝    ╚═════╝  ███████╗ ██╔════╝  ██║████═╗  ██║    ██ ║ ╚██████╔╝   ╚══════╝ 355 | 356 | 357 | NyA!K0 358 | 359 | 360 | Lu Yifei 361 | 362 | 363 | Kise Platinyl 364 | 365 | 366 | KS-OTO 367 | 368 | 369 | InfinityLoop 370 | 371 | 372 | Imgbot 373 | 374 | 375 | neveler 376 | 377 | 378 | 379 | > 贡献者列表,由 [GitHub Action][update-contributors-images] 自动生成 380 | 381 |
382 | 383 | [![][back-to-top]](#readme-top) 384 | 385 |
386 | 387 | 388 | ## 📈 项目统计 389 | 390 | 391 | 392 | 393 | 394 | Star History Chart 395 | 396 | 397 | 398 | ![Alt](https://repobeats.axiom.co/api/embed/ae4c378f0e6ec317654ec5c4e8b01218c734cd53.svg "Repobeats analytics image") 399 | 400 |
401 | 402 | [![][back-to-top]](#readme-top) 403 | 404 |
405 | 406 | ## 🎁 欢迎打赏 407 | [赞赏列表](https://github.com/maboloshi/maboloshi/issues/1) 408 | | 微信赞赏 | 支付宝赞赏 | 409 | | :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: | 410 | | WeChat QRcode
☕喝点咖啡继续干☕ | AliPay QRcode
🌶️来包辣条吧~🍪 | 411 | 412 | 413 | 414 | 415 | [back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square 416 | [github-project-link]: https://github.com/maboloshi/github-chinese "GitHub 中文化插件" 417 | [github-issues-link]: https://github.com/maboloshi/github-chinese/issues "议题" 418 | [github-issues-shield]: https://img.shields.io/github/issues/maboloshi/github-chinese?style=flat-square&logo=github&label=Issue 419 | [github-stars-link]: https://github.com/maboloshi/github-chinese/stargazers "星标" 420 | [github-stars-shield]: https://img.shields.io/github/stars/maboloshi/github-chinese?style=flat-square&logo=github&label=Star 421 | [github-forks-link]: https://github.com/maboloshi/github-chinese/network "复刻" 422 | [github-forks-shield]: https://img.shields.io/github/forks/maboloshi/github-chinese?style=flat-square&logo=github&label=Fork 423 | [github-license-link]: https://opensource.org/licenses/GPL-3.0 "许可证" 424 | [github-license-shield]: https://img.shields.io/github/license/maboloshi/github-chinese?style=flat-square&logo=github&label=License 425 | [greasyFork-link]: https://greasyfork.org/scripts/435208 "GreasyFork 源 - GitHub 中文化插件" 426 | [greasyFork-shield]: https://img.shields.io/greasyfork/dt/435208?style=flat-square&logo=GreasyFork&label=GreasyFork%20Installs 427 | [pr-welcome-link]: https://github.com/maboloshi/github-chinese/pulls 428 | [pr-welcome-shield]: https://img.shields.io/badge/🤯_pr_welcome-%E2%86%92-ffcb47?labelColor=black&style=for-the-badge "欢迎提交 PR" 429 | [Tampermonkey]: http://tampermonkey.net/ "篡改猴" 430 | [Violentmonkey]: https://violentmonkey.github.io/ "暴力猴" 431 | [Macaque]: https://macaque.app/ "猕猴" 432 | [Stay]: https://apps.apple.com/cn/app/stay-for-safari-%E6%B5%8F%E8%A7%88%E5%99%A8%E4%BC%B4%E4%BE%A3/id1591620171 "Stay" 433 | [main.user.js]: https://github.com/maboloshi/github-chinese/raw/gh-pages/main.user.js "GitHub 中文化插件 - GitHub 源" 434 | [main(greasyfork).user.js]: https://greasyfork.org/scripts/435208-github-%E4%B8%AD%E6%96%87%E5%8C%96%E6%8F%92%E4%BB%B6/code/GitHub%20%E4%B8%AD%E6%96%87%E5%8C%96%E6%8F%92%E4%BB%B6.user.js "GitHub 中文化插件 - GreasyFork 源" 435 | [update-contributors-images]: https://github.com/maboloshi/github-chinese/blob/gh-pages/.github/workflows/update_contributors_images.yml 436 | -------------------------------------------------------------------------------- /main(greasyfork).user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name GitHub 中文化插件 3 | // @namespace https://github.com/maboloshi/github-chinese 4 | // @description 中文化 GitHub 界面的部分菜单及内容。原作者为楼教主(http://www.52cik.com/)。 5 | // @copyright 2021, 沙漠之子 (https://maboloshi.github.io/Blog) 6 | // @icon https://github.githubassets.com/pinned-octocat.svg 7 | // @version 1.9.2-2025-12-10 8 | // @author 沙漠之子 9 | // @license GPL-3.0 10 | // @match https://github.com/* 11 | // @match https://skills.github.com/* 12 | // @match https://gist.github.com/* 13 | // @match https://www.githubstatus.com/* 14 | // @require https://greasyfork.org/scripts/435207-github-%E4%B8%AD%E6%96%87%E5%8C%96%E6%8F%92%E4%BB%B6-%E4%B8%AD%E6%96%87%E8%AF%8D%E5%BA%93%E8%A7%84%E5%88%99/code/GitHub%20%E4%B8%AD%E6%96%87%E5%8C%96%E6%8F%92%E4%BB%B6%20-%20%E4%B8%AD%E6%96%87%E8%AF%8D%E5%BA%93%E8%A7%84%E5%88%99.js?v1.9.2-2025-12-10 15 | // @run-at document-end 16 | // @grant GM_xmlhttpRequest 17 | // @grant GM_getValue 18 | // @grant GM_setValue 19 | // @grant GM_registerMenuCommand 20 | // @grant GM_unregisterMenuCommand 21 | // @grant GM_notification 22 | // @connect www.iflyrec.com 23 | // @supportURL https://github.com/maboloshi/github-chinese/issues 24 | // ==/UserScript== 25 | 26 | (function (window, document, undefined) { 27 | 'use strict'; 28 | 29 | const lang = I18N.zh ? 'zh' : 'zh-CN'; // 设置默认语言 30 | let page; 31 | let enable_RegExp = GM_getValue("enable_RegExp", 1); 32 | 33 | /** 34 | * watchUpdate 函数:监视页面变化,根据变化的节点进行翻译 35 | */ 36 | function watchUpdate() { 37 | // 检测浏览器是否支持 MutationObserver 38 | const MutationObserver = 39 | window.MutationObserver || 40 | window.WebKitMutationObserver || 41 | window.MozMutationObserver; 42 | 43 | // 获取当前页面的 URL 44 | const getCurrentURL = () => location.href; 45 | getCurrentURL.previousURL = getCurrentURL(); 46 | 47 | // 创建 MutationObserver 实例,监听 DOM 变化 48 | const observer = new MutationObserver((mutations, observer) => { 49 | const currentURL = getCurrentURL(); 50 | 51 | // 如果页面的 URL 发生变化 52 | if (currentURL !== getCurrentURL.previousURL) { 53 | getCurrentURL.previousURL = currentURL; 54 | page = getPage(); // 当页面地址发生变化时,更新全局变量 page 55 | console.log(`链接变化 page= ${page}`); 56 | 57 | transTitle(); // 翻译页面标题 58 | 59 | if (page) { 60 | setTimeout(() => { 61 | // 使用 CSS 选择器找到页面上的元素,并将其文本内容替换为预定义的翻译 62 | transBySelector(); 63 | if (page === "repository") { //仓库简介翻译 64 | transDesc(".f4.my-3"); 65 | } else if (page === "gist") { // Gist 简介翻译 66 | transDesc(".gist-content [itemprop='about']"); 67 | } 68 | }, 500); 69 | } 70 | } 71 | 72 | if (page) { 73 | // 使用 filter 方法对 mutations 数组进行筛选, 74 | // 返回 `节点增加、文本更新 或 属性更改的 mutation` 组成的新数组 filteredMutations。 75 | const filteredMutations = mutations.filter(mutation => mutation.addedNodes.length > 0 || mutation.type === 'attributes' || mutation.type === 'characterData'); 76 | 77 | // 处理每个变化 78 | filteredMutations.forEach(mutation => traverseNode(mutation.target)); 79 | } 80 | }); 81 | 82 | // 配置 MutationObserver 83 | const config = { 84 | characterData: true, 85 | subtree: true, 86 | childList: true, 87 | attributeFilter: ['value', 'placeholder', 'aria-label', 'data-confirm'], // 仅观察特定属性变化 88 | }; 89 | 90 | // 开始观察 document.body 的变化 91 | observer.observe(document.body, config); 92 | } 93 | 94 | /** 95 | * traverseNode 函数:遍历指定的节点,并对节点进行翻译。 96 | * @param {Node} node - 需要遍历的节点。 97 | */ 98 | function traverseNode(node) { 99 | // 跳过忽略 100 | if (I18N.conf.reIgnoreId.test(node.id) || 101 | I18N.conf.reIgnoreClass.test(node.className) || 102 | I18N.conf.reIgnoreTag.includes(node.tagName) || 103 | (node.getAttribute && I18N.conf.reIgnoreItemprop.test(node.getAttribute("itemprop"))) 104 | ) { 105 | return; 106 | } 107 | 108 | if (node.nodeType === Node.ELEMENT_NODE) { // 元素节点处理 109 | 110 | // 翻译时间元素 111 | if ( 112 | ["RELATIVE-TIME", "TIME-AGO", "TIME", "LOCAL-TIME"].includes(node.tagName) 113 | ) { 114 | if (node.shadowRoot) { 115 | transTimeElement(node.shadowRoot); 116 | watchTimeElement(node.shadowRoot); 117 | } else { 118 | transTimeElement(node); 119 | } 120 | return; 121 | } 122 | 123 | // 元素节点属性翻译 124 | if (["INPUT", "TEXTAREA"].includes(node.tagName)) { // 输入框 按钮 文本域 125 | if (["button", "submit", "reset"].includes(node.type)) { 126 | if (node.hasAttribute('data-confirm')) { // 翻译 浏览器 提示对话框 127 | transElement(node, 'data-confirm', true); 128 | } 129 | transElement(node, 'value'); 130 | } else { 131 | transElement(node, 'placeholder'); 132 | } 133 | } else if (node.tagName === 'BUTTON') { 134 | if (node.hasAttribute('aria-label') && /tooltipped/.test(node.className)) { 135 | transElement(node, 'aria-label', true); // 翻译 浏览器 提示对话框 136 | } 137 | if (node.hasAttribute('title')) { 138 | transElement(node, 'title', true); // 翻译 浏览器 提示对话框 139 | } 140 | if (node.hasAttribute('data-confirm')) { 141 | transElement(node, 'data-confirm', true); // 翻译 浏览器 提示对话框 ok 142 | } 143 | if (node.hasAttribute('data-confirm-text')) { 144 | transElement(node, 'data-confirm-text', true); // 翻译 浏览器 提示对话框 ok 145 | } 146 | if (node.hasAttribute('data-confirm-cancel-text')) { 147 | transElement(node, 'data-confirm-cancel-text', true); // 取消按钮 提醒 148 | } 149 | if (node.hasAttribute('cancel-confirm-text')) { 150 | transElement(node, 'cancel-confirm-text', true); // 取消按钮 提醒 151 | } 152 | if (node.hasAttribute('data-disable-with')) { // 按钮等待提示 153 | transElement(node, 'data-disable-with', true); 154 | } 155 | } else if (node.tagName === 'OPTGROUP') { // 翻译 的 label 属性 156 | transElement(node, 'label'); 157 | } else if (/tooltipped/.test(node.className)) { // 仅当 元素存在'tooltipped'样式 aria-label 才起效果 158 | transElement(node, 'aria-label', true); // 带提示的元素,类似 tooltip 效果的 159 | } else if (node.tagName === 'A') { 160 | if (node.hasAttribute('title')) { 161 | transElement(node, 'title', true); // 翻译 浏览器 提示对话框 162 | } 163 | if (node.hasAttribute('data-hovercard-type')) { 164 | return; // 不翻译 165 | } 166 | } 167 | 168 | let childNodes = node.childNodes; 169 | childNodes.forEach(traverseNode); // 遍历子节点 170 | 171 | } else if (node.nodeType === Node.TEXT_NODE) { // 文本节点翻译 172 | if (node.length <= 500) { // 修复 许可证编辑框初始化载入内容被翻译 173 | transElement(node, 'data'); 174 | } 175 | } 176 | } 177 | 178 | /** 179 | * getPage 函数:获取当前页面的类型。 180 | * @returns {string|boolean} 当前页面的类型,如果无法确定类型,那么返回 false。 181 | */ 182 | function getPage() { 183 | 184 | // 站点,如 gist, developer, help 等,默认主站是 github 185 | const siteMapping = { 186 | 'gist.github.com': 'gist', 187 | 'www.githubstatus.com': 'status', 188 | 'skills.github.com': 'skills' 189 | }; 190 | const site = siteMapping[location.hostname] || 'github'; // 站点 191 | const pathname = location.pathname; // 当前路径 192 | 193 | // 是否登录 194 | const isLogin = document.body.classList.contains("logged-in"); 195 | 196 | // 用于确定 个人首页,组织首页,仓库页 然后做判断 197 | const analyticsLocation = (document.getElementsByName('analytics-location')[0] || {}).content || ''; 198 | // 组织页 199 | const isOrganization = /\//.test(analyticsLocation) || /^\/(?:orgs|organizations)/.test(pathname); 200 | // 仓库页 201 | const isRepository = /\/\//.test(analyticsLocation); 202 | 203 | // 优先匹配 body 的 class 204 | let page, t = document.body.className.match(I18N.conf.rePageClass); 205 | if (t) { 206 | if (t[1] === 'page-profile') { 207 | let matchResult = location.search.match(/tab=(\w+)/); 208 | if (matchResult) { 209 | page = 'page-profile/' + matchResult[1]; 210 | } else { 211 | page = pathname.match(/\/(stars)/) ? 'page-profile/stars' : 'page-profile'; 212 | } 213 | } else { 214 | page = t[1]; 215 | } 216 | } else if (site === 'gist') { // Gist 站点 217 | page = 'gist'; 218 | } else if (site === 'status') { // GitHub Status 页面 219 | page = 'status'; 220 | } else if (site === 'skills') { // GitHub Skills 页面 221 | page = 'skills'; 222 | } else if (pathname === '/' && site === 'github') { // github.com 首页 223 | page = isLogin ? 'page-dashboard' : 'homepage'; 224 | } else if (isRepository) { // 仓库页 225 | t = pathname.match(I18N.conf.rePagePathRepo); 226 | page = t ? 'repository/' + t[1] : 'repository'; 227 | } else if (isOrganization) { // 组织页 228 | t = pathname.match(I18N.conf.rePagePathOrg); 229 | page = t ? 'orgs/' + (t[1] || t.slice(-1)[0]) : 'orgs'; 230 | } else { 231 | t = pathname.match(I18N.conf.rePagePath); 232 | page = t ? (t[1] || t.slice(-1)[0]) : false; // 取页面 key 233 | } 234 | 235 | if (!page || !I18N[lang][page]) { 236 | console.log(`请注意对应 page ${page} 词库节点不存在`); 237 | page = false; 238 | } 239 | return page; 240 | } 241 | 242 | /** 243 | * transTitle 函数:翻译页面标题 244 | */ 245 | function transTitle() { 246 | let key = document.title; // 标题文本内容 247 | let str = I18N[lang]['title']['static'][key] || ''; 248 | if (!str) { 249 | let res = I18N[lang]['title'].regexp || []; 250 | for (let [a, b] of res) { 251 | str = key.replace(a, b); 252 | if (str !== key) { 253 | break; 254 | } 255 | } 256 | } 257 | document.title = str; 258 | } 259 | 260 | /** 261 | * transTimeElement 函数:翻译时间元素文本内容。 262 | * @param {Element} el - 需要翻译的元素。 263 | */ 264 | function transTimeElement(el) { 265 | let key = el.childNodes.length > 0 ? el.lastChild.textContent : el.textContent; 266 | let res = I18N[lang]['public']['time-regexp']; // 时间正则规则 267 | 268 | for (let [a, b] of res) { 269 | let str = key.replace(a, b); 270 | if (str !== key) { 271 | el.textContent = str; 272 | break; 273 | } 274 | } 275 | } 276 | 277 | /** 278 | * watchTimeElement 函数:监视时间元素变化, 触发和调用时间元素翻译 279 | * @param {Element} el - 需要监视的元素。 280 | */ 281 | function watchTimeElement(el) { 282 | const MutationObserver = 283 | window.MutationObserver || 284 | window.WebKitMutationObserver || 285 | window.MozMutationObserver; 286 | 287 | new MutationObserver(mutations => { 288 | transTimeElement(mutations[0].addedNodes[0]); 289 | }).observe(el, { 290 | childList: true 291 | }); 292 | } 293 | 294 | /** 295 | * transElement 函数:翻译指定元素的文本内容或属性。 296 | * @param {Element} el - 需要翻译的元素。 297 | * @param {string} field - 需要翻译的文本内容或属性的名称。 298 | * @param {boolean} isAttr - 是否需要翻译属性。 299 | */ 300 | function transElement(el, field, isAttr = false) { 301 | let text = isAttr ? el.getAttribute(field) : el[field]; // 需要翻译的文本 302 | let str = translateText(text); // 翻译后的文本 303 | 304 | // 替换翻译后的内容 305 | if (str) { 306 | if (!isAttr) { 307 | el[field] = str; 308 | } else { 309 | el.setAttribute(field, str); 310 | } 311 | } 312 | } 313 | 314 | /** 315 | * translateText 函数:翻译文本内容。 316 | * @param {string} text - 需要翻译的文本内容。 317 | * @returns {string|boolean} 翻译后的文本内容,如果没有找到对应的翻译,那么返回 false。 318 | */ 319 | function translateText(text) { // 翻译 320 | 321 | // 内容为空, 空白字符和或数字, 不存在英文字母和符号,. 跳过 322 | if (!isNaN(text) || !/[a-zA-Z,.]+/.test(text)) { 323 | return false; 324 | } 325 | 326 | let _key = text.trim(); // 去除首尾空格的 key 327 | let _key_neat = _key.replace(/\xa0|[\s]+/g, ' ') // 去除多余空白字符(  空格 换行符) 328 | 329 | let str = fetchTranslatedText(_key_neat); // 翻译已知页面 (局部优先) 330 | 331 | if (str && str !== _key_neat) { // 已知页面翻译完成 332 | return text.replace(_key, str); // 替换原字符,保留首尾空白部分 333 | } 334 | 335 | return false; 336 | } 337 | 338 | /** 339 | * fetchTranslatedText 函数:从特定页面的词库中获得翻译文本内容。 340 | * @param {string} key - 需要翻译的文本内容。 341 | * @returns {string|boolean} 翻译后的文本内容,如果没有找到对应的翻译,那么返回 false。 342 | */ 343 | function fetchTranslatedText(key) { 344 | 345 | // 静态翻译 346 | let str = I18N[lang][page]['static'][key] || I18N[lang]['public']['static'][key]; // 默认翻译 公共部分 347 | 348 | if (typeof str === 'string') { 349 | return str; 350 | } 351 | 352 | // 正则翻译 353 | if (enable_RegExp) { 354 | let res = (I18N[lang][page].regexp || []).concat(I18N[lang]['public'].regexp || []); // 正则数组 355 | 356 | for (let [a, b] of res) { 357 | str = key.replace(a, b); 358 | if (str !== key) { 359 | return str; 360 | } 361 | } 362 | } 363 | 364 | return false; // 没有翻译条目 365 | } 366 | 367 | /** 368 | * transDesc 函数:为指定的元素添加一个翻译按钮,并为该按钮添加点击事件。 369 | * @param {string} el - CSS选择器,用于选择需要添加翻译按钮的元素。 370 | */ 371 | function transDesc(el) { 372 | // 使用 CSS 选择器选择元素 373 | let element = document.querySelector(el); 374 | 375 | // 如果元素不存在 或者 translate-me 元素已存在,那么直接返回 376 | if (!element || document.getElementById('translate-me')) { 377 | return false; 378 | } 379 | 380 | // 在元素后面插入一个翻译按钮 381 | const buttonHTML = `
翻译
`; 382 | element.insertAdjacentHTML('afterend', buttonHTML); 383 | let button = element.nextSibling; 384 | 385 | // 为翻译按钮添加点击事件 386 | button.addEventListener('click', () => { 387 | // 获取元素的文本内容 388 | const desc = element.textContent.trim(); 389 | 390 | // 如果文本内容为空,那么直接返回 391 | if (!desc) { 392 | return false; 393 | } 394 | 395 | // 调用 translateDescText 函数进行翻译 396 | translateDescText(desc, text => { 397 | // 翻译完成后,隐藏翻译按钮,并在元素后面插入翻译结果 398 | button.style.display = "none"; 399 | const translationHTML = `讯飞听见 翻译👇
${text}`; 400 | element.insertAdjacentHTML('afterend', translationHTML); 401 | }); 402 | }); 403 | } 404 | 405 | /** 406 | * translateDescText 函数:将指定的文本发送到讯飞的翻译服务进行翻译。 407 | * @param {string} text - 需要翻译的文本。 408 | * @param {function} callback - 翻译完成后的回调函数,该函数接受一个参数,即翻译后的文本。 409 | */ 410 | function translateDescText(text, callback) { 411 | // 使用 GM_xmlhttpRequest 函数发送 HTTP 请求 412 | GM_xmlhttpRequest({ 413 | method: "POST", // 请求方法为 POST 414 | url: "https://www.iflyrec.com/TranslationService/v1/textTranslation", // 请求的 URL 415 | headers: { // 请求头 416 | 'Content-Type': 'application/json', 417 | 'Origin': 'https://www.iflyrec.com', 418 | }, 419 | data: JSON.stringify({ 420 | "from": "2", 421 | "to": "1", 422 | "contents": [{ 423 | "text": text, 424 | "frontBlankLine": 0 425 | }] 426 | }), // 请求的数据 427 | responseType: "json", // 响应的数据类型为 JSON 428 | onload: (res) => { 429 | try { 430 | const { status, response } = res; 431 | const translatedText = (status === 200) ? response.biz[0].translateResult : "翻译失败"; 432 | callback(translatedText); 433 | } catch (error) { 434 | console.error('翻译失败', error); 435 | callback("翻译失败"); 436 | } 437 | }, 438 | onerror: (error) => { 439 | console.error('网络请求失败', error); 440 | callback("网络请求失败"); 441 | } 442 | }); 443 | } 444 | 445 | /** 446 | * transBySelector 函数:通过 CSS 选择器找到页面上的元素,并将其文本内容替换为预定义的翻译。 447 | */ 448 | function transBySelector() { 449 | // 获取当前页面的翻译规则,如果没有找到,那么使用公共的翻译规则 450 | let res = (I18N[lang][page]?.selector || []).concat(I18N[lang]['public'].selector || []); // 数组 451 | 452 | // 如果找到了翻译规则 453 | if (res.length > 0) { 454 | // 遍历每个翻译规则 455 | for (let [selector, translation] of res) { 456 | // 使用 CSS 选择器找到对应的元素 457 | let element = document.querySelector(selector) 458 | // 如果找到了元素,那么将其文本内容替换为翻译后的文本 459 | if (element) { 460 | element.textContent = translation; 461 | } 462 | } 463 | } 464 | } 465 | 466 | function registerMenuCommand() { 467 | const toggleRegExp = () => { 468 | enable_RegExp = !enable_RegExp; 469 | GM_setValue("enable_RegExp", enable_RegExp); 470 | GM_notification(`已${enable_RegExp ? '开启' : '关闭'}正则功能`); 471 | if (enable_RegExp) { 472 | location.reload(); 473 | } 474 | GM_unregisterMenuCommand(id); 475 | id = GM_registerMenuCommand(`${enable_RegExp ? '关闭' : '开启'}正则功能`, toggleRegExp); 476 | }; 477 | 478 | let id = GM_registerMenuCommand(`${enable_RegExp ? '关闭' : '开启'}正则功能`, toggleRegExp); 479 | } 480 | 481 | /** 482 | * init 函数:初始化翻译功能。 483 | */ 484 | function init() { 485 | // 获取当前页面的翻译规则 486 | page = getPage(); 487 | console.log(`开始page= ${page}`); 488 | 489 | // 翻译页面标题 490 | transTitle(); 491 | 492 | if (page) { 493 | // 立即翻译页面 494 | traverseNode(document.body); 495 | 496 | setTimeout(() => { 497 | // 使用 CSS 选择器找到页面上的元素,并将其文本内容替换为预定义的翻译 498 | transBySelector(); 499 | if (page === "repository") { //仓库简介翻译 500 | transDesc(".f4.my-3"); 501 | } else if (page === "gist") { // Gist 简介翻译 502 | transDesc(".gist-content [itemprop='about']"); 503 | } 504 | }, 100); 505 | } 506 | // 监视页面变化 507 | watchUpdate(); 508 | } 509 | 510 | // 执行初始化 511 | registerMenuCommand(); 512 | init(); 513 | 514 | })(window, document); 515 | -------------------------------------------------------------------------------- /main_zh-TW.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name GitHub 中文化插件(繁體版) 3 | // @namespace https://github.com/maboloshi/github-chinese 4 | // @description 中文化 GitHub 界面的部分菜單及內容。原作者為樓教主(http://www.52cik.com/)。 5 | // @copyright 2021, 沙漠之子 (https://maboloshi.github.io/Blog) 6 | // @icon https://github.githubassets.com/pinned-octocat.svg 7 | // @version 1.9.3-2025-12-10 8 | // @author 沙漠之子 9 | // @license GPL-3.0 10 | // @match https://github.com/* 11 | // @match https://skills.github.com/* 12 | // @match https://gist.github.com/* 13 | // @match https://education.github.com/* 14 | // @match https://www.githubstatus.com/* 15 | // @require https://raw.githubusercontent.com/maboloshi/github-chinese/gh-pages/locals_zh-TW.js?v1.9.3-2025-12-10 16 | // @run-at document-start 17 | // @grant GM_xmlhttpRequest 18 | // @grant GM_getValue 19 | // @grant GM_setValue 20 | // @grant GM_registerMenuCommand 21 | // @grant GM_unregisterMenuCommand 22 | // @grant GM_notification 23 | // @connect fanyi.iflyrec.com 24 | // @supportURL https://github.com/maboloshi/github-chinese/issues 25 | // ==/UserScript== 26 | 27 | (function (window, document, undefined) { 28 | 'use strict'; 29 | 30 | const lang = 'zh-TW'; // 設置默認語言 31 | let enable_RegExp = GM_getValue("enable_RegExp", 1), 32 | page = false, 33 | cachedPage = null, 34 | characterData = null, 35 | ignoreMutationSelectors = [], 36 | ignoreSelectors = [], 37 | tranSelectors = [], 38 | regexpRules = []; 39 | 40 | function updateConfig(page) { 41 | if (cachedPage !== page && page) { 42 | cachedPage = page; 43 | 44 | const { characterDataPage, ignoreMutationSelectorPage, ignoreSelectorPage } = I18N.conf; 45 | characterData = characterDataPage.includes(page); 46 | // 忽略突變元素選擇器 47 | ignoreMutationSelectors = ignoreMutationSelectorPage['*'].concat(ignoreMutationSelectorPage[page] || []); 48 | // 忽略元素選擇器 49 | ignoreSelectors = ignoreSelectorPage['*'].concat(ignoreSelectorPage[page] || []); 50 | // 通過 CSS 選擇器翻譯的規則 51 | tranSelectors = (I18N[lang][page]?.selector || []).concat(I18N[lang]['public'].selector || []); 52 | // 正則詞條 53 | regexpRules = (I18N[lang][page].regexp || []).concat(I18N[lang]['public'].regexp || []); 54 | } 55 | } 56 | 57 | function initPage() { 58 | const page = getPage(); 59 | updateConfig(page); 60 | return page; 61 | } 62 | 63 | /** 64 | * watchUpdate 函數:監視頁面變化,根據變化的節點進行翻譯 65 | */ 66 | function watchUpdate() { 67 | // 檢測瀏覽器是否支持 MutationObserver 68 | const MutationObserver = 69 | window.MutationObserver || 70 | window.WebKitMutationObserver || 71 | window.MozMutationObserver; 72 | 73 | // 緩存當前頁面的 URL 74 | let previousURL = location.href; 75 | 76 | // 監聽 document.body 下 DOM 變化,用於處理節點變化 77 | new MutationObserver(mutations => { 78 | const currentURL = location.href; 79 | 80 | // 如果頁面的 URL 發生變化 81 | if (currentURL !== previousURL) { 82 | previousURL = currentURL; 83 | page = initPage(); 84 | console.log(`DOM變化觸發: 鏈接變化 page= ${page}`); 85 | } 86 | 87 | if (page) { 88 | 89 | // 使用 mutations.flatMap 進行篩選突變: 90 | // 1. 針對`節點增加`突變,後期叠代翻譯的對象調整為`addedNodes`中記錄的新增節點,而不是`target`,此舉大幅減少重復叠代翻譯 91 | // 2. 對於其它`屬性`和特定頁面`文本節點`突變,仍舊直接處理`target` 92 | // 3. 使用`nodes.filter()`篩選丟棄特定頁面`特定忽略元素`內突變的節點 93 | const filteredMutations = mutations.flatMap(({ target, addedNodes, type }) => { 94 | let nodes = []; 95 | if (type === 'childList' && addedNodes.length > 0) { 96 | nodes = Array.from(addedNodes); // `節點增加`,將`addedNodes`轉換為數組 97 | } else if (type === 'attributes' || (characterData && type === 'characterData')) { 98 | nodes = [target]; // 否則,僅處理目標節點 99 | } 100 | 101 | // 對每個節點進行篩選,忽略特定選擇器 102 | return nodes.filter(node => 103 | !ignoreMutationSelectors.some(selector => node.parentElement?.closest(selector)) 104 | ); 105 | }); 106 | 107 | // 處理每個變化 108 | filteredMutations.forEach(node => traverseNode(node)); 109 | } 110 | }).observe(document.body, { 111 | characterData: true, 112 | subtree: true, 113 | childList: true, 114 | attributeFilter: ['value', 'placeholder', 'aria-label', 'data-confirm', 'data-visible-text'], // 僅觀察特定屬性變化 115 | }); 116 | } 117 | 118 | /** 119 | * traverseNode 函數:遍歷指定的節點,並對節點進行翻譯。 120 | * @param {Node} node - 需要遍歷的節點。 121 | */ 122 | function traverseNode(node) { 123 | // 跳過忽略 124 | const skipNode = node => ignoreSelectors.some(selector => node.matches?.(selector)); 125 | if (skipNode(node)) return; 126 | 127 | if (node.nodeType === Node.ELEMENT_NODE) { // 元素節點處理 128 | 129 | // 處理不同標簽的元素屬性翻譯 130 | switch (node.tagName) { 131 | case "RELATIVE-TIME": // 翻譯時間元素 132 | transTimeElement(node.shadowRoot); 133 | watchTimeElement(node.shadowRoot); 134 | return; 135 | 136 | case "INPUT": 137 | case "TEXTAREA": // 輸入框 按鈕 文本域 138 | if (['button', 'submit', 'reset'].includes(node.type)) { 139 | transElement(node.dataset, 'confirm'); // 翻譯 瀏覽器 提示對話框 140 | transElement(node, 'value'); 141 | } else { 142 | transElement(node, 'placeholder'); 143 | } 144 | break; 145 | 146 | case "BUTTON": 147 | if (/tooltipped/.test(node.className)) transElement(node, 'ariaLabel'); // 翻譯 瀏覽器 提示對話框 148 | transElement(node, 'title'); // 翻譯 瀏覽器 提示對話框 149 | transElement(node.dataset, 'confirm'); // 翻譯 瀏覽器 提示對話框 ok 150 | transElement(node.dataset, 'confirmText'); // 翻譯 瀏覽器 提示對話框 ok 151 | transElement(node.dataset, 'confirmCancelText'); // 取消按鈕 提醒 152 | transElement(node, 'cancelConfirmText'); // 取消按鈕 提醒 153 | transElement(node.dataset, 'disableWith'); // 按鈕等待提示 154 | break; 155 | 156 | case "OPTGROUP": 157 | transElement(node, 'label'); // 翻譯 的 label 屬性 158 | break; 159 | 160 | case "A": 161 | transElement(node, 'title'); // title 屬性 162 | transElement(node, 'ariaLabel'); // aria-label 屬性 163 | break; 164 | 165 | case "SPAN": 166 | transElement(node, 'title'); // title 屬性 167 | if (/tooltipped/.test(node.className)) transElement(node, 'ariaLabel'); 168 | transElement(node.dataset, 'visibleText'); // 按鈕提示 169 | break; 170 | 171 | default: 172 | // 僅當 元素存在'tooltipped'樣式 aria-label 才起效果 173 | if (/tooltipped/.test(node.className)) transElement(node, 'ariaLabel'); // 帶提示的元素,類似 tooltip 效果的 174 | } 175 | 176 | node.childNodes.forEach(child => traverseNode(child)); // 遍歷子節點 177 | 178 | } else if (node.nodeType === Node.TEXT_NODE && node.length <= 500) { // 文本節點翻譯 179 | transElement(node, 'data'); 180 | } 181 | } 182 | 183 | /** 184 | * getPage 函數:獲取頁面的類型。 185 | * @param {URL object} URL - 需要分析的 URL。 186 | * @returns {string|boolean} 頁面的類型,如果無法確定類型,那麽返回 false。 187 | */ 188 | function getPage(url = window.location) { 189 | // 站點映射 190 | const siteMapping = { 191 | 'gist.github.com': 'gist', 192 | 'www.githubstatus.com': 'status', 193 | 'skills.github.com': 'skills', 194 | 'education.github.com': 'education', 195 | }; 196 | const site = siteMapping[url.hostname] || 'github'; 197 | const pathname = url.pathname; 198 | 199 | // 是否登錄 200 | const isLogin = document.body.classList.contains("logged-in"); 201 | // 獲取 analytics-location 202 | const analyticsLocation = document.head.querySelector('meta[name="analytics-location"]')?.content || ''; 203 | 204 | // 判斷頁面類型 205 | const isOrganization = /\//.test(analyticsLocation) || /^\/(?:orgs|organizations)/.test(pathname); 206 | const isRepository = /\/\//.test(analyticsLocation); 207 | const isProfile = document.body.classList.contains("page-profile") || analyticsLocation === '/'; 208 | const isSession = document.body.classList.contains("session-authentication"); 209 | 210 | const { rePagePathRepo, rePagePathOrg, rePagePath } = I18N.conf; 211 | let t, page = false; 212 | 213 | if (isSession) { 214 | page = 'session-authentication'; 215 | } else if (site === 'gist' || site === 'status' || site === 'skills' || site === 'education') { 216 | page = site; 217 | } else if (isProfile) { 218 | t = url.search.match(/tab=([^&]+)/); 219 | page = t ? 'page-profile/' + t[1] : pathname.includes('/stars') ? 'page-profile/stars' : 'page-profile'; 220 | } else if (pathname === '/' && site === 'github') { 221 | page = isLogin ? 'page-dashboard' : 'homepage'; 222 | } else if (isRepository) { 223 | t = pathname.match(rePagePathRepo); 224 | page = t ? 'repository/' + t[1] : 'repository'; 225 | } else if (isOrganization) { 226 | t = pathname.match(rePagePathOrg); 227 | page = t ? 'orgs/' + (t[1] || t.slice(-1)[0]) : 'orgs'; 228 | } else { 229 | t = pathname.match(rePagePath); 230 | page = t ? (t[1] || t.slice(-1)[0]) : false; 231 | } 232 | 233 | if (!page || !I18N[lang][page]) { 234 | console.log(`請註意對應 page ${page} 詞庫節點不存在`); 235 | page = false; 236 | } 237 | return page; 238 | } 239 | 240 | /** 241 | * transTitle 函數:翻譯頁面標題 242 | */ 243 | function transTitle() { 244 | const text = document.title; // 標題文本內容 245 | let translatedText = I18N[lang]['title']['static'][text] || ''; 246 | if (!translatedText) { 247 | const res = I18N[lang]['title'].regexp || []; 248 | for (let [a, b] of res) { 249 | translatedText = text.replace(a, b); 250 | if (translatedText !== text) break; 251 | } 252 | } 253 | document.title = translatedText; 254 | } 255 | 256 | /** 257 | * transTimeElement 函數:翻譯時間元素文本內容。 258 | * @param {Element} el - 需要翻譯的元素。 259 | */ 260 | function transTimeElement(el) { 261 | const text = el.childNodes.length > 0 ? el.lastChild.textContent : el.textContent; 262 | const translatedText = text.replace(/^on/, ""); 263 | if (translatedText !== text) { 264 | el.textContent = translatedText; 265 | } 266 | } 267 | 268 | /** 269 | * watchTimeElement 函數:監視時間元素變化, 觸發和調用時間元素翻譯 270 | * @param {Element} el - 需要監視的元素。 271 | */ 272 | function watchTimeElement(el) { 273 | const MutationObserver = 274 | window.MutationObserver || 275 | window.WebKitMutationObserver || 276 | window.MozMutationObserver; 277 | 278 | new MutationObserver(mutations => { 279 | transTimeElement(mutations[0].addedNodes[0]); 280 | }).observe(el, { 281 | childList: true 282 | }); 283 | } 284 | 285 | /** 286 | * transElement 函數:翻譯指定元素的文本內容或屬性。 287 | * @param {Element|DOMStringMap} el - 需要翻譯的元素或元素的數據集 (node.dataset)。 288 | * @param {string} field - 需要翻譯的屬性名稱或文本內容字段。 289 | */ 290 | function transElement(el, field) { 291 | const text = el[field]; // 獲取需要翻譯的文本 292 | if (!text) return false; // 當 text 為空時,退出函數 293 | 294 | const translatedText = transText(text); // 翻譯後的文本 295 | if (translatedText) { 296 | el[field] = translatedText; // 替換翻譯後的內容 297 | } 298 | } 299 | 300 | /** 301 | * transText 函數:翻譯文本內容。 302 | * @param {string} text - 需要翻譯的文本內容。 303 | * @returns {string|boolean} 翻譯後的文本內容,如果沒有找到對應的翻譯,那麽返回 false。 304 | */ 305 | function transText(text) { 306 | // 判斷是否需要跳過翻譯 307 | // 1. 檢查內容是否為空或者僅包含空白字符或數字。 308 | // 2. 檢查內容是否僅包含中文字符。 309 | // 3. 檢查內容是否不包含英文字母和符號。 310 | const shouldSkip = text => /^[\s0-9]*$/.test(text) || /^[\u4e00-\u9fa5]+$/.test(text) || !/[a-zA-Z,.]/.test(text); 311 | if (shouldSkip(text)) return false; 312 | 313 | // 清理文本內容 314 | const trimmedText = text.trim(); // 去除首尾空格 315 | const cleanedText = trimmedText.replace(/\xa0|[\s]+/g, ' '); // 去除多余空白字符(包括   空格 換行符) 316 | 317 | // 嘗試獲取翻譯結果 318 | const translatedText = fetchTranslatedText(cleanedText); 319 | 320 | // 如果找到翻譯並且不與清理後的文本相同,則返回替換後的結果 321 | if (translatedText && translatedText !== cleanedText) { 322 | return text.replace(trimmedText, translatedText); // 替換原字符,保留首尾空白部分 323 | } 324 | 325 | return false; 326 | } 327 | 328 | /** 329 | * fetchTranslatedText 函數:從特定頁面的詞庫中獲得翻譯文本內容。 330 | * @param {string} text - 需要翻譯的文本內容。 331 | * @returns {string|boolean} 翻譯後的文本內容,如果沒有找到對應的翻譯,那麽返回 false。 332 | */ 333 | function fetchTranslatedText(text) { 334 | 335 | // 靜態翻譯 336 | let translatedText = I18N[lang][page]['static'][text] || I18N[lang]['public']['static'][text]; // 默認翻譯 公共部分 337 | 338 | if (typeof translatedText === 'string') { 339 | return translatedText; 340 | } 341 | 342 | // 正則翻譯 343 | if (enable_RegExp) { 344 | for (let [a, b] of regexpRules) { 345 | translatedText = text.replace(a, b); 346 | if (translatedText !== text) { 347 | return translatedText; 348 | } 349 | } 350 | } 351 | 352 | return false; // 沒有翻譯條目 353 | } 354 | 355 | /** 356 | * transDesc 函數:為指定的元素添加一個翻譯按鈕,並為該按鈕添加點擊事件。 357 | * @param {string} selector - CSS選擇器,用於選擇需要添加翻譯按鈕的元素。 358 | */ 359 | function transDesc(selector) { 360 | // 使用 CSS 選擇器選擇元素 361 | const element = document.querySelector(selector); 362 | 363 | // 如果元素不存在 或者 translate-me 元素已存在,那麽直接返回 364 | if (!element || document.getElementById('translate-me')) return false; 365 | 366 | // 在元素後面插入一個翻譯按鈕 367 | const buttonHTML = `
翻譯
`; 368 | element.insertAdjacentHTML('afterend', buttonHTML); 369 | const button = element.nextSibling; 370 | 371 | // 為翻譯按鈕添加點擊事件 372 | button.addEventListener('click', () => { 373 | // 獲取元素的文本內容 374 | const descText = element.textContent.trim(); 375 | 376 | // 如果文本內容為空,那麽直接返回 377 | if (!descText) return false; 378 | 379 | // 調用 transDescText 函數進行翻譯 380 | transDescText(descText, translatedText => { 381 | // 翻譯完成後,隱藏翻譯按鈕,並在元素後面插入翻譯結果 382 | button.style.display = "none"; 383 | const translatedHTML = `訊飛聽見 翻譯👇
${translatedText}`; 384 | element.insertAdjacentHTML('afterend', translatedHTML); 385 | }); 386 | }); 387 | } 388 | 389 | /** 390 | * transDescText 函數:將指定的文本發送到訊飛的翻譯服務進行翻譯。 391 | * @param {string} text - 需要翻譯的文本。 392 | * @param {function} callback - 翻譯完成後的回調函數,該函數接受一個參數,即翻譯後的文本。 393 | */ 394 | function transDescText(text, callback) { 395 | // 使用 GM_xmlhttpRequest 函數發送 HTTP 請求 396 | GM_xmlhttpRequest({ 397 | method: "POST", // 請求方法為 POST 398 | url: "https://fanyi.iflyrec.com/TJHZTranslationService/v2/textAutoTranslation", // 請求的 URL 399 | headers: { // 請求頭 400 | 'Content-Type': 'application/json', 401 | 'Origin': 'https://fanyi.iflyrec.com', 402 | }, 403 | data: JSON.stringify({ 404 | "from": 2, 405 | "to": 1, 406 | "type": 1, 407 | "contents": [{ 408 | "text": text 409 | }] 410 | }), // 請求的數據 411 | responseType: "json", // 響應的數據類型為 JSON 412 | onload: (res) => { 413 | try { 414 | const { status, response } = res; 415 | const translatedText = (status === 200) ? response.biz[0].sectionResult[0].dst : "翻譯失敗"; 416 | callback(translatedText); 417 | } catch (error) { 418 | console.error('翻譯失敗', error); 419 | callback("翻譯失敗"); 420 | } 421 | }, 422 | onerror: (error) => { 423 | console.error('網絡請求失敗', error); 424 | callback("網絡請求失敗"); 425 | } 426 | }); 427 | } 428 | 429 | /** 430 | * transBySelector 函數:通過 CSS 選擇器找到頁面上的元素,並將其文本內容替換為預定義的翻譯。 431 | */ 432 | function transBySelector() { 433 | if (tranSelectors.length > 0) { 434 | // 遍歷每個翻譯規則 435 | for (let [selector, translatedText] of tranSelectors) { 436 | // 使用 CSS 選擇器找到對應的元素 437 | const element = document.querySelector(selector); 438 | // 如果找到了元素,那麽將其文本內容替換為翻譯後的文本 439 | if (element) { 440 | element.textContent = translatedText; 441 | } 442 | } 443 | } 444 | } 445 | 446 | /** 447 | * registerMenuCommand 函數:註冊菜單。 448 | */ 449 | function registerMenuCommand() { 450 | const toggleRegExp = () => { 451 | enable_RegExp = !enable_RegExp; 452 | GM_setValue("enable_RegExp", enable_RegExp); 453 | GM_notification(`已${enable_RegExp ? '開啟' : '關閉'}正則功能`); 454 | if (enable_RegExp) { 455 | location.reload(); 456 | } 457 | GM_unregisterMenuCommand(id); 458 | id = GM_registerMenuCommand(`${enable_RegExp ? '關閉' : '開啟'}正則功能`, toggleRegExp); 459 | }; 460 | 461 | let id = GM_registerMenuCommand(`${enable_RegExp ? '關閉' : '開啟'}正則功能`, toggleRegExp); 462 | } 463 | 464 | /** 465 | * init 函數:初始化翻譯功能。 466 | */ 467 | function init() { 468 | // 獲取當前頁面的翻譯規則 469 | page = initPage(); 470 | console.log(`開始page= ${page}`); 471 | 472 | if (page) traverseNode(document.body); 473 | 474 | // 監視頁面變化 475 | watchUpdate(); 476 | } 477 | 478 | // 設置中文環境 479 | document.documentElement.lang = lang; 480 | 481 | // 監測 HTML Lang 值, 設置中文環境 482 | new MutationObserver(mutations => { 483 | if (document.documentElement.lang === "en") { 484 | document.documentElement.lang = lang; 485 | } 486 | }).observe(document.documentElement, { 487 | attributeFilter: ['lang'] 488 | }); 489 | 490 | // 監聽 Turbo 完成事件 491 | document.addEventListener('turbo:load', () => { 492 | if (page) { 493 | transTitle(); // 翻譯頁面標題 494 | transBySelector(); 495 | if (page === "repository") { //倉庫簡介翻譯 496 | transDesc(".f4.my-3"); 497 | } else if (page === "gist") { // Gist 簡介翻譯 498 | transDesc(".gist-content [itemprop='about']"); 499 | } 500 | } 501 | }); 502 | 503 | // 初始化菜單 504 | registerMenuCommand(); 505 | 506 | // 在頁面初始加載完成時執行 507 | window.addEventListener('DOMContentLoaded', init); 508 | 509 | })(window, document); 510 | -------------------------------------------------------------------------------- /main.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name GitHub 中文化插件 3 | // @namespace https://github.com/maboloshi/github-chinese 4 | // @description 中文化 GitHub 界面的部分菜单及内容。原作者为楼教主(http://www.52cik.com/)。 5 | // @copyright 2021, 沙漠之子 (https://maboloshi.github.io/Blog) 6 | // @icon https://github.githubassets.com/pinned-octocat.svg 7 | // @version 1.9.3-2025-12-10 8 | // @author 沙漠之子 9 | // @license GPL-3.0 10 | // @match https://github.com/* 11 | // @match https://skills.github.com/* 12 | // @match https://gist.github.com/* 13 | // @match https://education.github.com/* 14 | // @match https://www.githubstatus.com/* 15 | // @require https://raw.githubusercontent.com/maboloshi/github-chinese/gh-pages/locals.js?v1.9.3-2025-12-10 16 | // @run-at document-start 17 | // @grant GM_xmlhttpRequest 18 | // @grant GM_getValue 19 | // @grant GM_setValue 20 | // @grant GM_registerMenuCommand 21 | // @grant GM_unregisterMenuCommand 22 | // @grant GM_notification 23 | // @connect fanyi.iflyrec.com 24 | // @supportURL https://github.com/maboloshi/github-chinese/issues 25 | // ==/UserScript== 26 | 27 | (function (window, document, undefined) { 28 | 'use strict'; 29 | 30 | /****************** 全局配置区(开发者可修改部分) ******************/ 31 | const FeatureSet = { 32 | enable_RegExp: GM_getValue("enable_RegExp", true), 33 | enable_transDesc: GM_getValue("enable_transDesc", true), 34 | }; 35 | const CONFIG = { 36 | LANG: 'zh-CN', 37 | // 站点域名 -> 类型映射 38 | PAGE_MAP: { 39 | 'gist.github.com': 'gist', 40 | 'www.githubstatus.com': 'status', 41 | 'skills.github.com': 'skills', 42 | 'education.github.com': 'education' 43 | }, 44 | // 需要特殊处理的站点类型 45 | SPECIAL_SITES: ['gist', 'status', 'skills', 'education'], 46 | // 简介 css 筛选器规则 47 | DESC_SELECTORS: { 48 | repository: ".f4.my-3", 49 | gist: ".gist-content [itemprop='about']" 50 | }, 51 | OBSERVER_CONFIG: { 52 | childList: true, 53 | subtree: true, 54 | characterData: true, 55 | attributeFilter: ['value', 'placeholder', 'aria-label', 'data-confirm'] 56 | }, 57 | // 当前使用引擎(开发者可切换) 58 | transEngine: 'iflyrec', 59 | // 翻译引擎配置 60 | TRANS_ENGINES: { 61 | iflyrec: { 62 | name: '讯飞听见', 63 | url: 'https://fanyi.iflyrec.com/text-translate', 64 | url_api: 'https://fanyi.iflyrec.com/TJHZTranslationService/v2/textAutoTranslation', 65 | method: 'POST', 66 | headers: { 67 | 'Content-Type': 'application/json', 68 | 'Origin': 'https://fanyi.iflyrec.com' 69 | }, 70 | // 请求体数据结构 71 | getRequestData: (text) => ({ 72 | from: 2, 73 | to: 1, 74 | type: 1, 75 | contents: [{ text: text }] 76 | }), 77 | // 响应标识 78 | responseIdentifier: 'biz[0]?.sectionResult[0]?.dst', 79 | }, 80 | } 81 | }; 82 | 83 | let pageConfig = {}; 84 | 85 | // 初始化 86 | init(); 87 | 88 | // 更新页面设置 89 | function updatePageConfig(currentPageChangeTrigger) { 90 | const newType = detectPageType(); 91 | if (newType && newType !== pageConfig.currentPageType) { 92 | pageConfig = buildPageConfig(newType); 93 | } 94 | console.log(`【Debug】${currentPageChangeTrigger}触发, 页面类型为 ${pageConfig.currentPageType}`); 95 | } 96 | 97 | // 构建页面设置 pageConfig 对象 98 | function buildPageConfig(pageType = pageConfig.currentPageType) { 99 | return { 100 | // 当前页面类型 101 | currentPageType: pageType, 102 | // 页面标题静态词库 103 | titleStaticDict: { 104 | ...I18N[CONFIG.LANG].public.title.static, 105 | ...(I18N[CONFIG.LANG][pageType]?.title?.static || {}) 106 | }, 107 | // 页面标题正则词库 108 | titleRegexpRules: [ 109 | ...I18N[CONFIG.LANG].public.title.regexp, 110 | ...(I18N[CONFIG.LANG][pageType]?.title?.regexp || []) 111 | ], 112 | // 静态词库 113 | staticDict: { 114 | ...I18N[CONFIG.LANG].public.static, 115 | ...(I18N[CONFIG.LANG][pageType]?.static || {}) 116 | }, 117 | // 正则词库 118 | regexpRules: [ 119 | ...(I18N[CONFIG.LANG][pageType]?.regexp || []), 120 | ...I18N[CONFIG.LANG].public.regexp 121 | ], 122 | // 忽略突变元素选择器(字符串) 123 | ignoreMutationSelectors: [ 124 | ...I18N.conf.ignoreMutationSelectorPage['*'], 125 | ...(I18N.conf.ignoreMutationSelectorPage[pageType] || []) 126 | ].join(', '), 127 | // 忽略元素选择器规则(字符串) 128 | ignoreSelectors: [ 129 | ...I18N.conf.ignoreSelectorPage['*'], 130 | ...(I18N.conf.ignoreSelectorPage[pageType] || []) 131 | ].join(', '), 132 | // 字符数据监视开启规则(布尔值) 133 | characterData: I18N.conf.characterDataPage.includes(pageType), 134 | // CSS 选择器规则 135 | tranSelectors: [ 136 | ...(I18N[CONFIG.LANG].public.selector || []), 137 | ...(I18N[CONFIG.LANG][pageType]?.selector || []) 138 | ], 139 | }; 140 | } 141 | 142 | /** 143 | * watchUpdate 函数:监视页面变化,根据变化的节点进行翻译 144 | */ 145 | function watchUpdate() { 146 | // 缓存当前页面的 URL 147 | let previousURL = window.location.href; 148 | 149 | const handleUrlChange = () => { 150 | const currentURL = window.location.href; 151 | // 如果页面的 URL 发生变化 152 | if (currentURL !== previousURL) { 153 | previousURL = currentURL; 154 | updatePageConfig("DOM变化"); 155 | } 156 | } 157 | 158 | const processMutations = mutations => { 159 | // 平铺突变记录并过滤需要处理的节点(链式操作) 160 | // 使用 mutations.flatMap 进行筛选突变: 161 | // 1. 针对`节点增加`突变,后期迭代翻译的对象调整为`addedNodes`中记录的新增节点,而不是`target`,此举大幅减少重复迭代翻译 162 | // 2. 对于其它`属性`和特定页面`文本节点`突变,仍旧直接处理`target` 163 | // 3. 使用`.filter()`筛选丢弃特定页面`特定忽略元素`内突变的节点 164 | mutations.flatMap(({ target, addedNodes, type }) => { 165 | // 处理子节点添加的情况 166 | if (type === 'childList' && addedNodes.length > 0) { 167 | return [...addedNodes]; // 将新增节点转换为数组 168 | } 169 | // 处理属性和文本内容变更的情况 170 | return (type === 'attributes' || (type === 'characterData' && pageConfig.characterData)) 171 | ? [target] // 否则,仅处理目标节点 172 | : []; 173 | }) 174 | // 过滤需要忽略的突变节点 175 | .filter(node => 176 | // 剔除节点元素所在 DOM 树中匹配忽略选择器 177 | !(node.closest 178 | ? node.closest(pageConfig.ignoreMutationSelectors) 179 | : node.parentElement?.closest(pageConfig.ignoreMutationSelectors) 180 | ) 181 | ) 182 | // 处理每个变化 183 | .forEach(node => 184 | // 递归遍历节点树进行处理 185 | traverseNode(node) 186 | ); 187 | } 188 | 189 | // 监听 document.body 下 DOM 变化,用于处理节点变化 190 | new MutationObserver(mutations => { 191 | handleUrlChange(); 192 | if (pageConfig.currentPageType) processMutations(mutations); 193 | }).observe(document.body, CONFIG.OBSERVER_CONFIG); 194 | } 195 | 196 | /** 197 | * traverseNode 函数:遍历指定的节点,并对节点进行翻译。 198 | * @param {Node} node - 需要遍历的节点。 199 | */ 200 | function traverseNode(rootNode) { 201 | const start = performance.now(); 202 | 203 | const handleTextNode = node => { 204 | if (node.length > 500) return; 205 | transElement(node, 'data'); 206 | } 207 | 208 | // 如果 rootNode 是文本节点,直接处理 209 | if (rootNode.nodeType === Node.TEXT_NODE) { 210 | handleTextNode(rootNode); 211 | return; // 文本节点没有子节点,直接返回 212 | } 213 | 214 | const treeWalker = document.createTreeWalker( 215 | rootNode, 216 | NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, 217 | node => 218 | // 跳过忽略的节点 219 | node.matches?.(pageConfig.ignoreSelectors) 220 | ? NodeFilter.FILTER_REJECT 221 | : NodeFilter.FILTER_ACCEPT, 222 | ); 223 | 224 | const handleElement = node => { 225 | // 处理不同标签的元素属性翻译 226 | switch (node.tagName) { 227 | case "RELATIVE-TIME": // 翻译时间元素 228 | transTimeElement(node.shadowRoot); 229 | return; 230 | 231 | case "INPUT": 232 | case "TEXTAREA": // 输入框 按钮 文本域 233 | if (['button', 'submit', 'reset'].includes(node.type)) { 234 | transElement(node.dataset, 'confirm'); // 翻译 浏览器 提示对话框 235 | transElement(node, 'value'); 236 | } else { 237 | transElement(node, 'placeholder'); 238 | } 239 | break; 240 | 241 | case "OPTGROUP": 242 | transElement(node, 'label'); // 翻译 的 label 属性 243 | break; 244 | 245 | case "BUTTON": 246 | transElement(node, 'title'); // 翻译 浏览器 提示对话框 247 | transElement(node.dataset, 'confirm'); // 翻译 浏览器 提示对话框 ok 248 | transElement(node.dataset, 'confirmText'); // 翻译 浏览器 提示对话框 ok 249 | transElement(node.dataset, 'confirmCancelText'); // 取消按钮 提醒 250 | transElement(node, 'cancelConfirmText'); // 取消按钮 提醒 251 | transElement(node.dataset, 'disableWith'); // 按钮等待提示 252 | 253 | case "A": 254 | case "SPAN": 255 | transElement(node, 'title'); // title 属性 256 | transElement(node.dataset, 'visibleText'); // 翻译 浏览器 提示对话框 ok 257 | 258 | default: 259 | // 仅当 元素存在'tooltipped'样式 aria-label 才起效果 260 | if (/tooltipped/.test(node.className)) transElement(node, 'ariaLabel'); // 带提示的元素,类似 tooltip 效果的 261 | } 262 | } 263 | 264 | // 预绑定处理函数提升性能 265 | const handlers = { 266 | [Node.ELEMENT_NODE]: handleElement, 267 | [Node.TEXT_NODE]: handleTextNode 268 | }; 269 | 270 | let currentNode; 271 | while ((currentNode = treeWalker.nextNode())) { 272 | handlers[currentNode.nodeType]?.(currentNode); 273 | } 274 | 275 | const duration = performance.now() - start; 276 | if (duration > 10) { 277 | // console.warn(`【Debug】节点遍历耗时: ${duration.toFixed(2)}ms`, rootNode); 278 | console.log(`节点遍历耗时: ${duration.toFixed(2)}ms`); 279 | } 280 | } 281 | 282 | /** 283 | * detectPageType 函数:检测当前页面类型,基于URL、元素类名和meta信息。 284 | * @returns {string|boolean} 页面的类型,如'repository'、'dashboard'等,如果无法确定类型,那么返回 false。 285 | */ 286 | function detectPageType() { 287 | const url = new URL(window.location.href); 288 | const { PAGE_MAP, SPECIAL_SITES } = CONFIG; 289 | const { hostname, pathname } = url; 290 | 291 | // 基础配置 =============================================== 292 | const site = PAGE_MAP[hostname] || 'github'; // 通过站点映射获取基础类型 293 | const isLogin = document.body.classList.contains("logged-in"); 294 | const metaLocation = document.head.querySelector('meta[name="analytics-location"]')?.content || ''; 295 | 296 | // 页面特征检测 ============================================ 297 | const isSession = document.body.classList.contains("session-authentication"); 298 | const isHomepage = pathname === '/' && site === 'github'; 299 | const isProfile = document.body.classList.contains("page-profile") || metaLocation === '/'; 300 | const isRepository = /\/\//.test(metaLocation); 301 | const isOrganization = /\//.test(metaLocation) || /^\/(?:orgs|organizations)/.test(pathname); 302 | 303 | // 正则配置 ================================================ 304 | const { rePagePathRepo, rePagePathOrg, rePagePath } = I18N.conf; 305 | 306 | // 核心判断逻辑 ============================================ 307 | let pageType; 308 | switch (true) { // 使用 switch(true) 模式处理多条件分支 309 | // 1. 登录相关页面 310 | case isSession: 311 | pageType = 'session-authentication'; 312 | break; 313 | 314 | // 2. 特殊站点类型(gist/status/skills/education) 315 | case SPECIAL_SITES.includes(site): 316 | pageType = site; 317 | break; 318 | 319 | // 3. 个人资料页 320 | case isProfile: 321 | const tabParam = new URLSearchParams(url.search).get('tab'); 322 | pageType = pathname.includes('/stars') ? 'page-profile/stars' 323 | : tabParam ? `page-profile/${tabParam}` 324 | : 'page-profile'; 325 | break; 326 | 327 | // 4. 首页/仪表盘 328 | case isHomepage: 329 | pageType = isLogin ? 'dashboard' : 'homepage'; 330 | break; 331 | 332 | // 5. 代码仓库页 333 | case isRepository: 334 | const repoMatch = pathname.match(rePagePathRepo); 335 | pageType = repoMatch ? `repository/${repoMatch[1]}` : 'repository'; 336 | break; 337 | 338 | // 6. 组织页面 339 | case isOrganization: 340 | const orgMatch = pathname.match(rePagePathOrg); 341 | pageType = orgMatch ? `orgs/${orgMatch[1] || orgMatch.slice(-1)[0]}` : 'orgs'; 342 | break; 343 | 344 | // 7. 默认处理逻辑 345 | default: 346 | const pathMatch = pathname.match(rePagePath); 347 | pageType = pathMatch ? (pathMatch[1] || pathMatch.slice(-1)[0]) : false; 348 | } 349 | 350 | console.log(`【Debug】pathname = ${pathname}, site = ${site}, isLogin = ${isLogin}, analyticsLocation = ${metaLocation}, isOrganization = ${isOrganization}, isRepository = ${isRepository}, isProfile = ${isProfile}, isSession = ${isSession}`) 351 | 352 | // 词库校验 ================================================ 353 | if (pageType === false || !I18N[CONFIG.LANG]?.[pageType]) { 354 | console.warn(`[i18n] 页面类型未匹配或词库缺失: ${pageType}`); 355 | return false; // 明确返回 false 表示异常 356 | } 357 | 358 | return pageType; 359 | } 360 | 361 | /** 362 | * transTitle 函数:翻译页面标题 363 | */ 364 | function transTitle() { 365 | const text = document.title; // 获取标题文本内容 366 | let translatedText = pageConfig.titleStaticDict[text] || ''; 367 | if (!translatedText) { 368 | for (const [pattern, replacement] of pageConfig.titleRegexpRules) { 369 | translatedText = text.replace(pattern, replacement); 370 | if (translatedText !== text) break; 371 | } 372 | } 373 | if (translatedText) { 374 | document.title = translatedText; 375 | } 376 | } 377 | 378 | /** 379 | * transTimeElement 函数:翻译时间元素文本内容。 380 | * @param {Element} el - 需要翻译的元素。 381 | */ 382 | function transTimeElement(el) { 383 | const text = el.childNodes.length > 0 ? el.lastChild.textContent : el.textContent; 384 | const translatedText = text.replace(/^on/, ""); 385 | if (translatedText !== text) { 386 | el.textContent = translatedText; 387 | } 388 | } 389 | 390 | /** 391 | * transElement 函数:翻译指定元素的文本内容或属性。 392 | * @param {Element|DOMStringMap} el - 需要翻译的元素或元素的数据集 (node.dataset)。 393 | * @param {string} field - 需要翻译的属性名称或文本内容字段。 394 | */ 395 | function transElement(el, field) { 396 | const text = el[field]; // 获取需要翻译的文本 397 | if (!text) return false; // 当 text 为空时,退出函数 398 | 399 | const translatedText = transText(text); // 翻译后的文本 400 | if (translatedText) { 401 | el[field] = translatedText; // 替换翻译后的内容 402 | } 403 | } 404 | 405 | /** 406 | * transText 函数:翻译文本内容。 407 | * @param {string} text - 需要翻译的文本内容。 408 | * @returns {string|boolean} 翻译后的文本内容,如果没有找到对应的翻译,那么返回 false。 409 | */ 410 | function transText(text) { 411 | // 判断是否需要跳过翻译 412 | // 1. 检查内容是否为空或者仅包含空白字符或数字。 413 | // 2. 检查内容是否仅包含中文字符。 414 | // 3. 检查内容是否不包含英文字母和符号。 415 | const shouldSkip = text => /^[\s0-9]*$/.test(text) || /^[\u4e00-\u9fa5]+$/.test(text) || !/[a-zA-Z,.]/.test(text); 416 | if (shouldSkip(text)) return false; 417 | 418 | // 清理文本内容 419 | const trimmedText = text.trim(); // 去除首尾空格 420 | const cleanedText = trimmedText.replace(/\xa0|[\s]+/g, ' '); // 去除多余空白字符(包括   空格 换行符) 421 | 422 | // 尝试获取翻译结果 423 | const translatedText = fetchTranslatedText(cleanedText); 424 | 425 | // 如果找到翻译并且不与清理后的文本相同,则返回替换后的结果 426 | if (translatedText && translatedText !== cleanedText) { 427 | return text.replace(trimmedText, translatedText); // 替换原字符,保留首尾空白部分 428 | } 429 | 430 | return false; 431 | } 432 | 433 | /** 434 | * fetchTranslatedText 函数:从特定页面的词库中获得翻译文本内容。 435 | * @param {string} text - 需要翻译的文本内容。 436 | * @returns {string|boolean} 翻译后的文本内容,如果没有找到对应的翻译,那么返回 false。 437 | */ 438 | function fetchTranslatedText(text) { 439 | 440 | // 静态翻译 441 | let translatedText = pageConfig.staticDict[text]; // 默认翻译 公共部分 442 | 443 | if (typeof translatedText === 'string') return translatedText; 444 | 445 | // 正则翻译 446 | if (FeatureSet.enable_RegExp) { 447 | for (const [pattern, replacement] of pageConfig.regexpRules) { 448 | translatedText = text.replace(pattern, replacement); 449 | if (translatedText !== text) return translatedText; 450 | } 451 | } 452 | 453 | return false; // 没有翻译条目 454 | } 455 | 456 | /** 457 | * transDesc 函数:为指定的元素添加一个翻译按钮,并为该按钮添加点击事件。 458 | * @param {string} selector - CSS选择器,用于选择需要添加翻译按钮的元素。 459 | */ 460 | function transDesc(selector) { 461 | // 使用 CSS 选择器选择元素 462 | const element = document.querySelector(selector); 463 | 464 | // 如果元素不存在 或者 translate-me 元素已存在,那么直接返回 465 | if (!element || element.nextElementSibling?.id === 'translate-me') return; 466 | 467 | // 在元素后面插入一个翻译按钮 468 | const button = document.createElement('div'); 469 | button.id = 'translate-me'; 470 | button.style.cssText = 'color: #1b95e0; font-size: small; cursor: pointer;'; 471 | button.textContent = '翻译'; 472 | element.after(button); 473 | 474 | // 为翻译按钮添加点击事件 475 | button.addEventListener('click', async() => { 476 | if (button.disabled) return; 477 | button.disabled = true; 478 | try { 479 | const descText = element.textContent.trim(); 480 | if (!descText) return; 481 | 482 | // 执行翻译 483 | const translatedText = await requestRemoteTranslation(descText); 484 | 485 | // 安全创建结果元素 486 | const { name, url } = CONFIG.TRANS_ENGINES[CONFIG.transEngine]; 487 | const resultContainer = document.createElement('div'); 488 | resultContainer.innerHTML = ` 489 | 490 | 由 ${name} 翻译👇 491 | 492 |
493 | `; 494 | // 安全插入文本内容 495 | const textNode = document.createTextNode(translatedText); 496 | resultContainer.appendChild(textNode); 497 | 498 | button.remove(); 499 | element.after(resultContainer); 500 | } finally { 501 | button.disabled = false; 502 | } 503 | }); 504 | } 505 | 506 | /** 507 | * getNestedProperty 函数:获取嵌套属性的安全函数 508 | * @param {Object} obj - 需要查询的对象 509 | * @param {string} path - 属性路径,例如 'biz[0].sectionResult[0].dst' 510 | * @returns {*} - 返回嵌套属性的值 511 | */ 512 | function getNestedProperty(obj, path) { 513 | return path.split('.').reduce((acc, part) => { 514 | const match = part.match(/(\w+)(?:\[(\d+)\])?/); 515 | if (!match) return undefined; 516 | const key = match[1]; 517 | const index = match[2]; 518 | if (acc && acc[key] !== undefined) { 519 | return index !== undefined ? acc[key][index] : acc[key]; 520 | } 521 | return undefined; 522 | }, obj); 523 | } 524 | 525 | /** 526 | * requestRemoteTranslation 函数:将指定的文本发送到设定的翻译引擎进行翻译。 527 | * @param {string} text - 需要翻译的文本。 528 | */ 529 | async function requestRemoteTranslation(text) { 530 | return new Promise((resolve) => { 531 | const { url_api, method, headers, getRequestData, responseIdentifier } = CONFIG.TRANS_ENGINES[CONFIG.transEngine]; 532 | // 构建请求数据 533 | const requestData = getRequestData(text); 534 | 535 | // 使用 GM_xmlhttpRequest 函数发送 HTTP 请求 536 | GM_xmlhttpRequest({ 537 | method: method, 538 | url: url_api, // 请求的 URL 539 | headers: headers, 540 | data: method === 'POST' ? JSON.stringify(requestData) : null, 541 | params: method === 'GET' ? requestData : null, // For GET requests 542 | onload: (res) => { 543 | try { 544 | const result = JSON.parse(res.responseText); 545 | console.log(result); 546 | const translatedText = getNestedProperty(result, responseIdentifier) || '翻译失败'; 547 | resolve(translatedText); 548 | } catch (err) { 549 | console.error('翻译失败:', err); 550 | resolve(`翻译失败(${err.type})`); 551 | } 552 | }, 553 | onerror: (err) => { 554 | console.error('翻译请求失败:', err); 555 | resolve(`翻译失败(${err.type})`); 556 | } 557 | }); 558 | }); 559 | } 560 | 561 | /** 562 | * transBySelector 函数:通过 CSS 选择器找到页面上的元素,并将其文本内容替换为预定义的翻译。 563 | */ 564 | function transBySelector() { 565 | // 遍历每个翻译规则 566 | pageConfig.tranSelectors?.forEach(([selector, translatedText]) => { 567 | // 使用 CSS 选择器找到对应的元素 568 | const element = document.querySelector(selector); 569 | // 如果找到了元素,那么将其文本内容替换为翻译后的文本 570 | if (element) { 571 | element.textContent = translatedText; 572 | } 573 | }) 574 | } 575 | 576 | /** 577 | * registerMenuCommand 函数:注册菜单。 578 | */ 579 | function registerMenuCommand() { 580 | const createMenuCommand = (config) => { 581 | const { label, key, callback } = config; 582 | let menuId; 583 | 584 | const getMenuLabel = (label, isEnabled) => 585 | `${isEnabled ? "禁用" : "启用"} ${label}`; 586 | 587 | const toggle = () => { 588 | const newFeatureState = !FeatureSet[key]; 589 | GM_setValue(key, newFeatureState); 590 | FeatureSet[key] = newFeatureState; 591 | GM_notification(`${label}已${newFeatureState ? '启用' : '禁用'}`); 592 | 593 | // 调用回调函数 594 | if (callback) callback(newFeatureState); 595 | 596 | // 更新菜单命令的标签 597 | GM_unregisterMenuCommand(menuId); 598 | menuId = GM_registerMenuCommand( 599 | getMenuLabel(label, newFeatureState), 600 | toggle 601 | ); 602 | }; 603 | 604 | // 初始注册菜单命令 605 | menuId = GM_registerMenuCommand( 606 | getMenuLabel(label, FeatureSet[key]), 607 | toggle 608 | ); 609 | }; 610 | 611 | const menuConfigs = [ 612 | { 613 | label: "正则功能", 614 | key: "enable_RegExp", 615 | callback: newFeatureState => { 616 | if (newFeatureState) traverseNode(document.body); 617 | } 618 | }, 619 | { 620 | label: "描述翻译", 621 | key: "enable_transDesc", 622 | callback: newFeatureState => { 623 | if (newFeatureState && CONFIG.DESC_SELECTORS[pageConfig.currentPageType]) { 624 | transDesc(CONFIG.DESC_SELECTORS[pageConfig.currentPageType]); 625 | } else { 626 | document.getElementById('translate-me')?.remove(); 627 | } 628 | } 629 | } 630 | ]; 631 | 632 | // 注册所有菜单项 633 | menuConfigs.forEach(config => createMenuCommand(config)); 634 | }; 635 | 636 | /** 637 | * init 函数:初始化翻译功能。 638 | */ 639 | function init() { 640 | if (typeof I18N === 'undefined') { 641 | alert('GitHub 汉化插件:词库文件 locals.js 未加载,脚本无法运行!'); 642 | // 也可以选择 return 或 throw new Error 643 | } else { 644 | console.log(`词库文件 locals.js 已加载`); 645 | } 646 | // 设置中文环境 647 | document.documentElement.lang = CONFIG.LANG; 648 | 649 | // 监测 HTML Lang 值, 设置中文环境 650 | new MutationObserver(() => { 651 | if (document.documentElement.lang === "en") { 652 | document.documentElement.lang = CONFIG.LANG; 653 | } 654 | }).observe(document.documentElement, { attributeFilter: ['lang'] }); 655 | 656 | // 监听 Turbo 完成事件(延迟翻译) 657 | document.addEventListener('turbo:load', () => { 658 | if (!pageConfig.currentPageType) return; 659 | 660 | transTitle(); // 翻译页面标题 661 | transBySelector(); 662 | 663 | if (FeatureSet.enable_transDesc && CONFIG.DESC_SELECTORS[pageConfig.currentPageType]) { 664 | transDesc(CONFIG.DESC_SELECTORS[pageConfig.currentPageType]); 665 | } 666 | }); 667 | 668 | // 初始化菜单 669 | registerMenuCommand(); 670 | 671 | 672 | // 首次页面翻译 673 | window.addEventListener('DOMContentLoaded', () => { 674 | // 获取当前页面的翻译规则 675 | updatePageConfig('首次载入'); 676 | if (pageConfig.currentPageType) traverseNode(document.body); 677 | 678 | // 监视页面变化 679 | watchUpdate(); 680 | }); 681 | } 682 | 683 | })(window, document); 684 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------