├── crates
└── installer
│ ├── assets
│ └── .gitkeep
│ ├── .gitignore
│ ├── Cargo.toml
│ └── src
│ ├── utils.rs
│ ├── vcc.rs
│ └── main.rs
├── .github
├── FUNDING.yml
└── workflows
│ └── release.yml
├── .prettierrc
├── images
├── screenshot_1.png
└── screenshot_2.png
├── types
├── injector.d.ts
└── patch
│ ├── translate.d.ts
│ ├── index.d.ts
│ └── algolia.d.ts
├── .env_example
├── .vscode
└── extensions.json
├── src
├── components
│ ├── setting
│ │ └── index.tsx
│ └── loading
│ │ ├── styles.module.css.d.ts
│ │ ├── styles.module.d.css.ts
│ │ ├── styles.module.css
│ │ └── index.tsx
├── patch
│ ├── console_log.ts
│ ├── algolia.ts
│ └── translate.ts
├── helpers.ts
├── injector.ts
└── patch-loader.ts
├── .gitignore
├── Cargo.toml
├── scripts
├── version.ts
├── localization_hashs.ts
└── build-patch-loader.ts
├── crowdin.yml
├── tsconfig.json
├── package.json
├── LICENSE
├── localization
├── algolia.zh_CN.json
├── algolia.zh_TW.json
├── algolia.en.json
├── zh_CN.json
├── zh_TW.json
└── en.json
├── Makefile
├── CONTRIBUTING.md
├── README.md
├── Cargo.lock
├── CODE_OF_CONDUCT.md
└── pnpm-lock.yaml
/crates/installer/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: ["https://afdian.com/a/gizmo"]
2 |
--------------------------------------------------------------------------------
/crates/installer/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
3 | assets/*.js
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "printWidth": 100
5 | }
6 |
--------------------------------------------------------------------------------
/images/screenshot_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gizmo-ds/vcc-auto-translate/main/images/screenshot_1.png
--------------------------------------------------------------------------------
/images/screenshot_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gizmo-ds/vcc-auto-translate/main/images/screenshot_2.png
--------------------------------------------------------------------------------
/types/injector.d.ts:
--------------------------------------------------------------------------------
1 | export interface InjectFunction {
2 | name: string
3 | type: 'jsx' | 'createElement'
4 | }
5 |
--------------------------------------------------------------------------------
/.env_example:
--------------------------------------------------------------------------------
1 | DEBUG_MODE=false
2 | EMBED_LANGUAGES=true
3 |
4 | ALGOLIA_APIKEY=
5 | ALGOLIA_APPID=
6 | ALGOLIA_INDEXNAME=
7 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "rust-lang.rust-analyzer",
4 | "esbenp.prettier-vscode"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/setting/index.tsx:
--------------------------------------------------------------------------------
1 | export function SettingsComponent() {
2 | return {
3 | component:
Settings
,
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/types/patch/translate.d.ts:
--------------------------------------------------------------------------------
1 | export interface Language {
2 | name: string
3 | language: string
4 | hash: string
5 | content: any
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | build
4 | test-data
5 | .env
6 | .env.local
7 | .vscode/*
8 | !.vscode/extensions.json
9 | target
10 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "2"
3 | members = ["crates/installer"]
4 |
5 | [profile.release]
6 | lto = true
7 | opt-level = 'z'
8 | panic = 'abort'
9 | strip = true
10 |
--------------------------------------------------------------------------------
/src/components/loading/styles.module.css.d.ts:
--------------------------------------------------------------------------------
1 | declare const ClassNames: {
2 | "dark": string;
3 | "light": string;
4 | "patchLoadingCover": string;
5 | "patchLoadingProgress": string;
6 | "patchLoadingText": string;
7 | };
8 | export default ClassNames;
--------------------------------------------------------------------------------
/src/components/loading/styles.module.d.css.ts:
--------------------------------------------------------------------------------
1 | declare const ClassNames: {
2 | "dark": string;
3 | "light": string;
4 | "patchLoadingCover": string;
5 | "patchLoadingProgress": string;
6 | "patchLoadingText": string;
7 | };
8 | export default ClassNames;
--------------------------------------------------------------------------------
/scripts/version.ts:
--------------------------------------------------------------------------------
1 | import { exec } from 'node:child_process'
2 | import { promisify } from 'node:util'
3 |
4 | export const getPatchVersion = promisify(exec)('git describe --tags --always')
5 | .then(({ stdout }) => JSON.stringify(stdout.trimEnd()))
6 | .catch(() => '""')
7 |
--------------------------------------------------------------------------------
/types/patch/index.d.ts:
--------------------------------------------------------------------------------
1 | export interface Config {
2 | patch_jsx?: FunctionPatch
3 | patch_createElement?: FunctionPatch
4 | patch_useMemo?: FunctionPatch
5 | before?: () => Promise
6 | after?: () => Promise
7 | }
8 |
9 | export interface FunctionPatch {
10 | fname: string
11 | before?: () => Promise
12 | after?: () => Promise
13 | }
14 |
--------------------------------------------------------------------------------
/crowdin.yml:
--------------------------------------------------------------------------------
1 | pull_request_title: 'chore: sync translations from crowdin'
2 | pull_request_labels:
3 | - translations
4 | commit_message: 'chore: synced translations from crowdin [skip ci]'
5 | append_commit_message: false
6 |
7 | files:
8 | - source: /localization/en.json
9 | translation: /localization/%locale_with_underscore%.json
10 | - source: /localization/algolia.en.json
11 | translation: /localization/algolia.%locale_with_underscore%.json
12 |
--------------------------------------------------------------------------------
/src/patch/console_log.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '@/types/patch'
2 |
3 | const trash_logs = [(args: any[]) => args && args.length > 0 && args[0] === 'got backend message']
4 |
5 | const config: Config = {
6 | async after() {
7 | const originalLog = console.log
8 | console.log = (...args: any[]) => {
9 | for (const fn of trash_logs) if (fn(args)) return
10 | originalLog.apply(console, args)
11 | }
12 | },
13 | }
14 |
15 | export default config
16 |
--------------------------------------------------------------------------------
/crates/installer/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "installer"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | anyhow = { version = "1.0.81" }
8 | regex-automata = { version = "0.4.6", default-features = false, features = ["std", "meta"] }
9 | scopeguard = { version = "1.2.0", default-features = false }
10 |
11 | [target.'cfg(windows)'.dependencies]
12 | winreg = { version = "0.52.0" }
13 |
14 | [[bin]]
15 | name = "vcc-auto-translate-installer"
16 | path = "src/main.rs"
17 |
--------------------------------------------------------------------------------
/crates/installer/src/utils.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | #[cfg(target_os = "windows")]
4 | pub fn verbatim_path_display(path: &PathBuf) -> String {
5 | let display = path.display().to_string();
6 | if display.starts_with(r#"\\?\UNC"#) {
7 | return display;
8 | }
9 | display.replace(r#"\\?\"#, "").replace("\\", "/")
10 | }
11 |
12 | #[cfg(not(target_os = "windows"))]
13 | pub fn verbatim_path_display(path: &PathBuf) -> String {
14 | path.display().to_string()
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "ESNext",
4 | "moduleResolution": "Node",
5 | "target": "ES2022",
6 | "strictNullChecks": true,
7 | "strictFunctionTypes": true,
8 | "sourceMap": true,
9 | "resolveJsonModule": true,
10 | "allowSyntheticDefaultImports": true,
11 | "allowArbitraryExtensions": true,
12 | "jsx": "react-jsx",
13 | "jsxImportSource": "jsx-dom",
14 | "paths": {
15 | "@/*": ["./*"]
16 | }
17 | },
18 | "exclude": ["node_modules", "**/node_modules/*", "test-data", "build"]
19 | }
20 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | import { get as kv_get, set as kv_set, createStore as create_store } from 'idb-keyval'
2 |
3 | export const store = create_store('vcc_auto_translate', 'store')
4 | export const DebugMode = process.env.DEBUG_MODE === 'true'
5 |
6 | // 本地化文件的哈希值
7 | export const localization_hashs: Record =
8 | //@ts-ignore
9 | vcc_auto_translate.localization_hashs ?? {}
10 |
11 | // 打印调试信息
12 | export function debug_log(...args: any[]) {
13 | if (!DebugMode) return
14 | console.debug('[vcc-auto-translate]', ...args)
15 | }
16 |
17 | export async function user_language(): Promise {
18 | return (await kv_get('user_language', store)) ?? navigator.language
19 | }
20 | export async function set_user_language(lang: string) {
21 | return kv_set('user_language', lang, store)
22 | }
23 |
--------------------------------------------------------------------------------
/scripts/localization_hashs.ts:
--------------------------------------------------------------------------------
1 | import { readdirSync, readFileSync } from 'node:fs'
2 | import { resolve, join as path_join } from 'node:path'
3 |
4 | export async function localization_hashs() {
5 | const dir = resolve('./localization')
6 | const files = readdirSync(dir)
7 | const hashs: Record = {}
8 | for (const filename of files)
9 | hashs[filename] = await text_hash(
10 | JSON.stringify(JSON.parse(readFileSync(path_join(dir, filename), 'utf-8')))
11 | )
12 | return JSON.stringify(hashs)
13 | }
14 |
15 | async function text_hash(content: any) {
16 | const hash = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(content as string))
17 | return hex(hash)
18 | }
19 | function hex(hash: ArrayBuffer): string {
20 | return Array.from(new Uint8Array(hash))
21 | .map((b) => b.toString(16).padStart(2, '0'))
22 | .join('')
23 | }
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vcc-auto-translate",
3 | "version": "1.0.0",
4 | "description": "",
5 | "private": true,
6 | "main": "index.js",
7 | "packageManager": "pnpm@10.6.1",
8 | "scripts": {
9 | "build:patch-loader": "esno scripts/build-patch-loader.ts"
10 | },
11 | "keywords": [],
12 | "author": {
13 | "name": "Gizmo",
14 | "email": "me@aika.dev"
15 | },
16 | "license": "MIT",
17 | "dependencies": {
18 | "@babel/types": "^7.24.0",
19 | "@fluentui/web-components": "^2.5.17",
20 | "ast-walker-scope": "^0.6.1",
21 | "idb-keyval": "^6.2.1",
22 | "jsx-dom": "^8.1.3",
23 | "magic-string-ast": "^0.3.0"
24 | },
25 | "devDependencies": {
26 | "@types/node": "^20.11.28",
27 | "dotenv": "^16.4.5",
28 | "esbuild": "^0.20.2",
29 | "esbuild-css-modules-plugin": "^3.1.0",
30 | "esno": "^4.7.0"
31 | },
32 | "pnpm": {
33 | "ignoredBuiltDependencies": [
34 | "esbuild"
35 | ],
36 | "onlyBuiltDependencies": [
37 | "esbuild"
38 | ]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Gizmo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/localization/algolia.zh_CN.json:
--------------------------------------------------------------------------------
1 | {
2 | "placeholder": "搜索 VRChat 文档",
3 | "translations": {
4 | "buttonText": "搜索",
5 | "buttonAriaLabel": "搜索",
6 | "resetButtonTitle": "清除关键词",
7 | "resetButtonAriaLabel": "清除关键词",
8 | "cancelButtonText": "取消",
9 | "cancelButtonAriaLabel": "取消",
10 | "searchInputLabel": "搜索",
11 | "recentSearchesTitle": "最近搜索",
12 | "noRecentSearchesText": "最近没有搜索任何关键词",
13 | "saveRecentSearchButtonTitle": "保存搜索关键词",
14 | "removeRecentSearchButtonTitle": "从搜索历史记录移除该关键词",
15 | "favoriteSearchesTitle": "收藏",
16 | "removeFavoriteSearchButtonTitle": "从收藏中移除该关键词",
17 | "titleText": "无法加载搜索结果",
18 | "helpText": "您可能需要检查一下您的网络连接。",
19 | "selectText": "选择搜索结果",
20 | "selectKeyAriaLabel": "回车键",
21 | "navigateText": "切换搜索结果",
22 | "navigateUpKeyAriaLabel": "上箭头",
23 | "navigateDownKeyAriaLabel": "下箭头",
24 | "closeText": "关闭搜索",
25 | "closeKeyAriaLabel": "Esc 键",
26 | "searchByText": "搜索服务提供商",
27 | "noResultsText": "该关键词没有搜索结果",
28 | "suggestedQueryText": "尝试搜索",
29 | "reportMissingResultsText": "你认为该查询应该有结果?",
30 | "reportMissingResultsLinkText": "告诉我们。"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags: [v*]
6 | branches: [main]
7 | pull_request:
8 |
9 | permissions:
10 | contents: write
11 |
12 | jobs:
13 | release:
14 | name: Build and release
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | - name: Install pnpm
20 | uses: pnpm/action-setup@v4.1.0
21 |
22 | - name: Setup Node.js
23 | uses: actions/setup-node@v4
24 | with:
25 | node-version: lts/*
26 | cache: pnpm
27 |
28 | - name: Install dependencies
29 | run: pnpm install
30 |
31 | - name: Install linker
32 | shell: bash
33 | run: sudo apt-get install -y mingw-w64
34 |
35 | - name: Install Rust
36 | uses: dtolnay/rust-toolchain@stable
37 | with:
38 | toolchain: stable
39 | target: x86_64-pc-windows-gnu
40 |
41 | - name: Build release
42 | run: cp .env_example .env && make
43 |
44 | - name: Create release
45 | uses: softprops/action-gh-release@v1
46 | if: startsWith(github.ref, 'refs/tags/')
47 | with:
48 | files: build/*
49 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | pm = $(if $(shell command -v bun 2> /dev/null), bun, pnpm)
2 |
3 | all: build-installer compress
4 |
5 | build-script-loader:
6 | make build-patch-loader
7 |
8 | build-patch-loader:
9 | @${pm} esno scripts/build-patch-loader.ts
10 | @rm -rf build/*.css
11 |
12 | build-installer: build-patch-loader
13 | @cp build/patch-loader.js crates/installer/assets/patch-loader.js
14 | @cargo build --release --locked --target x86_64-pc-windows-gnu
15 | @cp target/x86_64-pc-windows-gnu/release/vcc-auto-translate-installer.exe build/
16 |
17 | sha256sum:
18 | @rm -f build/*.sha256; for file in build/*; do sha256sum $$file > $$file.sha256; done
19 |
20 | compress:
21 | @if [ -n "$(shell command -v upx 2> /dev/null)" ]; then for file in build/*.exe; do upx $$file; done; fi
22 |
23 | clean:
24 | @rm -f cmd/installer/vcc-auto-translate.js
25 | @rm -f cmd/installer/localization/*.json
26 | @rm -rf build
27 | @cargo clean
28 |
29 | dev: clean build-patch-loader
30 | @rm -f crates/installer/assets/patch-loader.js
31 | @cp build/patch-loader.js crates/installer/assets/patch-loader.js
32 | @cargo build -p installer --locked --target x86_64-pc-windows-gnu
33 | target/x86_64-pc-windows-gnu/debug/vcc-auto-translate-installer.exe --no-pause
34 |
--------------------------------------------------------------------------------
/localization/algolia.zh_TW.json:
--------------------------------------------------------------------------------
1 | {
2 | "placeholder": "搜尋 VRChat 文檔",
3 | "translations": {
4 | "buttonText": "搜尋",
5 | "buttonAriaLabel": "搜尋",
6 | "resetButtonTitle": "清除查詢",
7 | "resetButtonAriaLabel": "清除查詢",
8 | "cancelButtonText": "取消",
9 | "cancelButtonAriaLabel": "取消",
10 | "searchInputLabel": "搜尋",
11 | "recentSearchesTitle": "Recent",
12 | "noRecentSearchesText": "尚無最近的搜尋紀錄",
13 | "saveRecentSearchButtonTitle": "儲存這個搜尋",
14 | "removeRecentSearchButtonTitle": "從歷史記錄中刪除這個搜尋",
15 | "favoriteSearchesTitle": "收藏",
16 | "removeFavoriteSearchButtonTitle": "從我的最愛移除此搜尋",
17 | "titleText": "無法抓取結果",
18 | "helpText": "你可能需要檢查網絡連接。",
19 | "selectText": "to select",
20 | "selectKeyAriaLabel": "Enter key",
21 | "navigateText": "to navigate",
22 | "navigateUpKeyAriaLabel": "Arrow up",
23 | "navigateDownKeyAriaLabel": "Arrow down",
24 | "closeText": "to close",
25 | "closeKeyAriaLabel": "Escape key",
26 | "searchByText": "Search by",
27 | "noResultsText": "No results for",
28 | "suggestedQueryText": "Try searching for",
29 | "reportMissingResultsText": "Believe this query should return results?",
30 | "reportMissingResultsLinkText": "Let us know."
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/localization/algolia.en.json:
--------------------------------------------------------------------------------
1 | {
2 | "placeholder": "Search the VRChat docs",
3 | "translations": {
4 | "buttonText": "Search",
5 | "buttonAriaLabel": "Search",
6 | "resetButtonTitle": "Clear the query",
7 | "resetButtonAriaLabel": "Clear the query",
8 | "cancelButtonText": "Cancel",
9 | "cancelButtonAriaLabel": "Cancel",
10 | "searchInputLabel": "Search",
11 | "recentSearchesTitle": "Recent",
12 | "noRecentSearchesText": "No recent searches",
13 | "saveRecentSearchButtonTitle": "Save this search",
14 | "removeRecentSearchButtonTitle": "Remove this search from history",
15 | "favoriteSearchesTitle": "Favorite",
16 | "removeFavoriteSearchButtonTitle": "Remove this search from favorites",
17 | "titleText": "Unable to fetch results",
18 | "helpText": "You might want to check your network connection.",
19 | "selectText": "to select",
20 | "selectKeyAriaLabel": "Enter key",
21 | "navigateText": "to navigate",
22 | "navigateUpKeyAriaLabel": "Arrow up",
23 | "navigateDownKeyAriaLabel": "Arrow down",
24 | "closeText": "to close",
25 | "closeKeyAriaLabel": "Escape key",
26 | "searchByText": "Search by",
27 | "noResultsText": "No results for",
28 | "suggestedQueryText": "Try searching for",
29 | "reportMissingResultsText": "Believe this query should return results?",
30 | "reportMissingResultsLinkText": "Let us know."
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/scripts/build-patch-loader.ts:
--------------------------------------------------------------------------------
1 | import { config as dotenv_config } from 'dotenv'
2 | import { build as esbuild } from 'esbuild'
3 | //@ts-ignore
4 | import cssModulesPlugin from 'esbuild-css-modules-plugin'
5 | import { localization_hashs } from './localization_hashs'
6 | import { rmSync } from 'node:fs'
7 | import { getPatchVersion } from './version'
8 |
9 | const env_config = dotenv_config({ path: ['.env.local', '.env'] })
10 |
11 | async function build() {
12 | const define = env_config.parsed
13 | ? Object.keys(env_config.parsed).reduce((acc, key) => {
14 | acc[`process.env.${key}`] = JSON.stringify(env_config.parsed![key])
15 | return acc
16 | }, {})
17 | : {}
18 | define['vcc_auto_translate.localization_hashs'] =
19 | env_config.parsed?.EMBED_LANGUAGES === 'true' ? await localization_hashs() : '{}'
20 | define['process.env.PATCH_VERSION'] = await getPatchVersion
21 |
22 | await esbuild({
23 | entryPoints: ['src/patch-loader.ts'],
24 | bundle: true,
25 | format: 'esm',
26 | platform: 'browser',
27 | target: 'es2022',
28 | minify: true,
29 | outfile: 'build/patch-loader.js',
30 | metafile: true,
31 | jsx: 'automatic',
32 | define,
33 | plugins: [
34 | cssModulesPlugin({
35 | emitDeclarationFile: true,
36 | inject: true,
37 | }),
38 | ],
39 | })
40 | rmSync('build/patch-loader.css')
41 | }
42 |
43 | build()
44 |
--------------------------------------------------------------------------------
/src/components/loading/styles.module.css:
--------------------------------------------------------------------------------
1 | .patch-loading-cover {
2 | --patch-loading-cover-dark-background: #1f1f1f;
3 | --patch-loading-cover-dark-color: #ffffff;
4 |
5 | --patch-loading-cover-light-background: #fafafa;
6 | --patch-loading-cover-light-color: #242424;
7 | }
8 |
9 | .patch-loading-cover {
10 | background: var(--patch-loading-cover-light-background);
11 | color: var(--patch-loading-cover-light-color) !important;
12 | }
13 |
14 | @media (prefers-color-scheme: dark) {
15 | .patch-loading-cover {
16 | background: var(--patch-loading-cover-dark-background);
17 | color: var(--patch-loading-cover-dark-color) !important;
18 | }
19 | }
20 |
21 | .patch-loading-cover.dark {
22 | background: var(--patch-loading-cover-dark-background) !important;
23 | color: var(--patch-loading-cover-dark-color) !important;
24 | }
25 |
26 | .patch-loading-cover.light {
27 | background: var(--patch-loading-cover-light-background);
28 | color: var(--patch-loading-cover-light-color) !important;
29 | }
30 |
31 | .patch-loading-cover {
32 | position: absolute;
33 | top: 0;
34 | left: 0;
35 |
36 | display: flex;
37 | align-items: center;
38 | justify-content: center;
39 | flex-direction: column;
40 |
41 | height: 100vh;
42 | width: 100vw;
43 |
44 | z-index: 1000;
45 | }
46 |
47 | .patch-loading-text {
48 | font-size: xx-large;
49 | }
50 |
51 | .patch-loading-progress {
52 | margin-top: 1rem;
53 | width: 400px;
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/loading/index.tsx:
--------------------------------------------------------------------------------
1 | import { fluentProgress, provideFluentDesignSystem } from '@fluentui/web-components'
2 | import styles from './styles.module.css'
3 |
4 | export function LoadingComponent(props: { text: string }) {
5 | provideFluentDesignSystem().register(fluentProgress())
6 |
7 | const app_theme = localStorage.getItem('app_theme')
8 | let color_scheme = window.matchMedia('(prefers-color-scheme: dark)').matches
9 | ? styles.dark
10 | : styles.light
11 | switch (app_theme) {
12 | case 'Dark':
13 | color_scheme = styles.dark
14 | break
15 | case 'Light':
16 | color_scheme = styles.light
17 | break
18 | default:
19 | break
20 | }
21 | return {
22 | component: (
23 |
24 |
25 | {props.text}
26 |
27 | {/* @ts-ignore */}
28 |
29 |
30 | ),
31 | set_text(text: string) {
32 | const text_elements = document.getElementsByClassName(styles.patchLoadingText)
33 | if (text_elements) for (const e of text_elements) e.innerHTML = text
34 | },
35 | hide_progress() {
36 | const progress_elements = document.getElementsByClassName(styles.patchLoadingProgress)
37 | if (progress_elements) for (const e of progress_elements) e.remove()
38 | },
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to [vcc-auto-translate](https://github.com/gizmo-ds/vcc-auto-translate)
2 |
3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
4 |
5 | - Reporting [an issue](https://github.com/gizmo-ds/vcc-auto-translate/issues/new?assignees=&labels=bug).
6 | - [Discussing](https://github.com/gizmo-ds/vcc-auto-translate/discussions) the current state of the code.
7 | - Submitting [a fix](https://github.com/gizmo-ds/vcc-auto-translate/compare).
8 | - Proposing [new features](https://github.com/gizmo-ds/vcc-auto-translate/issues/new?assignees=&labels=enhancement).
9 | - Becoming a maintainer.
10 |
11 | ## All Changes Happen Through Pull Requests
12 |
13 | Pull requests are the best way to propose changes. We actively welcome your pull requests:
14 |
15 | 1. Fork the repo and create your branch from `main`.
16 | 2. If you've added code that should be tested, add some tests' examples.
17 | 3. If you've changed APIs, update the documentation.
18 | 4. Issue that pull request!
19 |
20 | ## Any contributions you make will be under the MIT Software License
21 |
22 | In short, when you submit changes, your submissions are understood to be under the same [MIT License](https://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
23 |
24 | ## Report issues/bugs using GitHub's [issues](https://github.com/gizmo-ds/vcc-auto-translate/issues)
25 |
26 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/gizmo-ds/vcc-auto-translate/issues/new/choose); it's that easy!
27 |
--------------------------------------------------------------------------------
/types/patch/algolia.d.ts:
--------------------------------------------------------------------------------
1 | export interface Localization {
2 | translations: DocSearchTranslations
3 | placeholder: string
4 | }
5 |
6 | interface DocSearchTranslations
7 | extends ButtonTranslations,
8 | SearchBoxTranslations,
9 | FooterTranslations,
10 | ErrorScreenTranslations,
11 | StartScreenTranslations,
12 | NoResultsScreenTranslations {
13 | placeholder?: string
14 | }
15 |
16 | // https://github.com/algolia/docsearch/blob/2df2e1392fa80e5cc9cafac3437685331b9f07ec/packages/docsearch-react/src/DocSearchButton.tsx#L6-L9
17 | type ButtonTranslations = Partial<{
18 | buttonText: string
19 | buttonAriaLabel: string
20 | }>
21 |
22 | // https://github.com/algolia/docsearch/blob/2df2e1392fa80e5cc9cafac3437685331b9f07ec/packages/docsearch-react/src/SearchBox.tsx#L14-L20
23 | type SearchBoxTranslations = Partial<{
24 | resetButtonTitle: string
25 | resetButtonAriaLabel: string
26 | cancelButtonText: string
27 | cancelButtonAriaLabel: string
28 | searchInputLabel: string
29 | }>
30 |
31 | // https://github.com/algolia/docsearch/blob/2df2e1392fa80e5cc9cafac3437685331b9f07ec/packages/docsearch-react/src/Footer.tsx#L5-L14
32 | type FooterTranslations = Partial<{
33 | selectText: string
34 | selectKeyAriaLabel: string
35 | navigateText: string
36 | navigateUpKeyAriaLabel: string
37 | navigateDownKeyAriaLabel: string
38 | closeText: string
39 | closeKeyAriaLabel: string
40 | searchByText: string
41 | }>
42 |
43 | // https://github.com/algolia/docsearch/blob/2df2e1392fa80e5cc9cafac3437685331b9f07ec/packages/docsearch-react/src/ErrorScreen.tsx#L5-L8
44 | type ErrorScreenTranslations = Partial<{
45 | titleText: string
46 | helpText: string
47 | }>
48 |
49 | // https://github.com/algolia/docsearch/blob/2df2e1392fa80e5cc9cafac3437685331b9f07ec/packages/docsearch-react/src/StartScreen.tsx#L8-L15
50 | type StartScreenTranslations = Partial<{
51 | recentSearchesTitle: string
52 | noRecentSearchesText: string
53 | saveRecentSearchButtonTitle: string
54 | removeRecentSearchButtonTitle: string
55 | favoriteSearchesTitle: string
56 | removeFavoriteSearchButtonTitle: string
57 | }>
58 |
59 | // https://github.com/algolia/docsearch/blob/2df2e1392fa80e5cc9cafac3437685331b9f07ec/packages/docsearch-react/src/NoResultsScreen.tsx#L7-L12
60 | type NoResultsScreenTranslations = Partial<{
61 | noResultsText: string
62 | suggestedQueryText: string
63 | reportMissingResultsText: string
64 | reportMissingResultsLinkText: string
65 | }>
66 |
--------------------------------------------------------------------------------
/src/injector.ts:
--------------------------------------------------------------------------------
1 | import { InjectFunction } from '@/types/injector'
2 | import { AssignmentExpression, MemberExpression } from '@babel/types'
3 | import { walkAST, babelParse } from 'ast-walker-scope'
4 | import { MagicString } from 'magic-string-ast'
5 |
6 | const propertys = ['jsx', 'createElement']
7 |
8 | export async function injector(code: string, funcs: InjectFunction[]): Promise {
9 | return new Promise(async (resolve, reject) => {
10 | const filters: string[] = [...new Set(funcs.map((f) => f.type))].filter((n) =>
11 | propertys.includes(n)
12 | )
13 | if (filters.length === 0) return resolve(code)
14 | const nodes: Record = filters.reduce((acc, key) => {
15 | acc[key] = undefined
16 | return acc
17 | }, {})
18 |
19 | const ms = new MagicString(code)
20 | const ast = babelParse(code)
21 |
22 | let skip = false
23 | walkAST(ast, {
24 | enter(node) {
25 | if (skip) return this.skip()
26 |
27 | if (filters.length > 0) {
28 | if (
29 | node.type === 'ExpressionStatement' &&
30 | node.expression.type === 'AssignmentExpression' &&
31 | node.expression.operator === '=' &&
32 | node.expression.left.type === 'MemberExpression' &&
33 | node.expression.left.property.type === 'Identifier' &&
34 | node.expression.right.type === 'Identifier'
35 | ) {
36 | const name = node.expression.left.property.name
37 | if (!filters.includes(name)) return this.skip()
38 | nodes[name] = node.expression
39 | if (Object.keys(nodes).every((n) => nodes[n] !== undefined)) skip = true
40 | if (propertys.every((n) => nodes[n] !== undefined)) skip = true
41 | return this.skip()
42 | }
43 | }
44 | },
45 | })
46 |
47 | for (const name of filters) {
48 | funcs
49 | .filter((f) => f.type === name)
50 | .forEach((f) => {
51 | const node = nodes[name]!
52 | const obj = (node.left as MemberExpression).object
53 | //@ts-ignore
54 | const n = `${obj.name}.${node.left.property.name}`
55 | ms.appendRight(node.right.end!, `;${n}=__vcc_function_proxy__(${n},${f.name})`)
56 | })
57 | }
58 |
59 | resolve(ms.toString())
60 | })
61 | }
62 |
63 | export function function_proxy(ofn: Function, fn1: Function, fn2: Function) {
64 | return new Proxy(ofn, {
65 | apply: function (target, thisArg, argumentsList) {
66 | fn1 && fn1.apply(thisArg, argumentsList)
67 | const result = target.apply(thisArg, argumentsList)
68 | if (fn2) return fn2(result)
69 | return result
70 | },
71 | })
72 | }
73 |
--------------------------------------------------------------------------------
/crates/installer/src/vcc.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{Context, Result};
2 | use regex_automata::meta;
3 |
4 | #[cfg(target_os = "windows")]
5 | pub fn install_path() -> Result {
6 | use winreg::RegKey;
7 | let install_path: String = RegKey::predef(winreg::enums::HKEY_CURRENT_USER)
8 | .open_subkey("Software\\VCC")?
9 | .get_value("InstallPath")
10 | .with_context(|| "Failed to get VCC install path")?;
11 | Ok(install_path)
12 | }
13 |
14 | #[cfg(not(target_os = "windows"))]
15 | pub fn install_path() -> Result {
16 | Ok("./".to_string())
17 | }
18 |
19 | pub fn patch_loader() -> Result {
20 | let script_content = include_bytes!("../assets/patch-loader.js");
21 | let script_content = std::str::from_utf8(script_content.as_ref())
22 | .with_context(|| "Failed to convert patch loader asset to string")?;
23 | let loader_content = format!(
24 | r#""#,
29 | script_content
30 | );
31 | Ok(loader_content)
32 | }
33 |
34 | pub fn installer_csp(index_content: String) -> Result {
35 | let mut index_content = index_content.replace(
36 | "",
37 | "");
38 |
39 | let loader_content = patch_loader()?;
40 | let loader_content = format!("\n{}", loader_content);
41 |
42 | index_content = index_content.replace("", loader_content.as_str());
43 | Ok(index_content)
44 | }
45 |
46 | pub fn installer_meta(index_content: String) -> Result {
47 | let re = meta::Regex::new(r#"\/script]+>"#)?;
48 |
49 | let mut caps = re.create_captures();
50 | re.captures(&index_content, &mut caps);
51 | if !caps.is_match() {
52 | return Err(anyhow::anyhow!("Failed to find index.js script tag"));
53 | }
54 | if caps.group_len() < 2 {
55 | return Err(anyhow::anyhow!("Failed to find index.js script tag"));
56 | }
57 |
58 | let index_tag = match caps.get_group(0) {
59 | Some(span) => &index_content[span.start..span.end],
60 | None => return Err(anyhow::anyhow!("Failed to find index.js script tag")),
61 | };
62 | let index_file = match caps.get_group(1) {
63 | Some(span) => &index_content[span.start..span.end],
64 | None => return Err(anyhow::anyhow!("Failed to find index.js script tag")),
65 | };
66 |
67 | let index_content = index_content.replace(
68 | index_tag,
69 | &format!("", index_file),
70 | );
71 |
72 | let loader_content = patch_loader()?;
73 | let index_content =
74 | index_content.replace("