Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b550edd
Preparation to convert LiuChan to a unit testable package.
Paperfeed Sep 5, 2018
76c59dc
Converted to Typescript.
Paperfeed Sep 21, 2018
6ebbb34
Kind of lost track of what I was doing
Paperfeed Jun 6, 2019
bb71295
initial rewrite of entire extension and update to manifest v3 (extrem…
Paperfeed May 20, 2024
f93b473
Add TailwindCSS
Paperfeed May 29, 2024
0058fb1
Migrated to WXT
Paperfeed May 31, 2024
2dd6c45
Fix some WXT related changes
Paperfeed May 31, 2024
05c0a36
Updated options page controls and added color pickers
Paperfeed May 31, 2024
5e11502
Accidentally didn't commit options page
Paperfeed May 31, 2024
7477b96
Added tests for dictionary (and found minor issue with regexp due to it)
Paperfeed May 31, 2024
d2d41e1
Added theming support
Paperfeed Jun 6, 2024
b6d8fab
Improve dictionary parsing and entry conversion, added some tests
Paperfeed Jun 6, 2024
08b9e1a
Fixed some key handling, allow selection while hovering (still somewh…
Paperfeed Jun 6, 2024
b5c1738
Add test workflow
Paperfeed Jun 6, 2024
c18984a
build: migrate to wxt 0.21 and pnpm
Paperfeed Aug 4, 2026
d6c745a
feat: rebuild popup, options, and dictionary runtime
Paperfeed Aug 4, 2026
bb146e9
test: add regression coverage for mv3 extension flows
Paperfeed Aug 4, 2026
af836ac
ci: switch Chrome Web Store release to service accounts
Paperfeed Aug 4, 2026
8c8d2eb
build: add eslint dependency to package.json
Paperfeed Aug 4, 2026
3b3e285
fix: declare eslint for CI lint step
Paperfeed Aug 4, 2026
e0564a2
merge: bring rewrite up to date with master
Paperfeed Aug 4, 2026
aef2558
chore: remove duplicate dictionary data and unused textures
Paperfeed Aug 4, 2026
b849091
build: bump version to 3.0.0
Paperfeed Aug 4, 2026
5a5cb6b
ci: avoid duplicate verification for pull requests
Paperfeed Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Release to Chrome Web Store

on:
workflow_dispatch:
inputs:
dry_run:
description: Authenticate without uploading or submitting
required: true
default: true
type: boolean

concurrency:
group: chrome-web-store-release
cancel-in-progress: false

jobs:
submit:
runs-on: ubuntu-latest
environment: chrome-web-store
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Require master for a real submission
if: ${{ !inputs.dry_run && github.ref != 'refs/heads/master' }}
run: exit 1
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run compile
- run: pnpm run lint
- run: pnpm run test:unit
- run: pnpm run zip
- uses: actions/upload-artifact@v4
with:
name: liuchan-chrome-${{ github.sha }}
path: .output/*-chrome.zip
if-no-files-found: error
- name: Verify Chrome Web Store credentials
if: ${{ inputs.dry_run }}
run: pnpm run submit -- --dry-run --chrome-zip .output/*-chrome.zip
env:
CHROME_API_VERSION: v2
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CHROME_PUBLISHER_ID: ${{ secrets.CHROME_PUBLISHER_ID }}
CHROME_SERVICE_ACCOUNT_CLIENT_EMAIL: ${{ secrets.CHROME_SERVICE_ACCOUNT_CLIENT_EMAIL }}
CHROME_SERVICE_ACCOUNT_PRIVATE_KEY: ${{ secrets.CHROME_SERVICE_ACCOUNT_PRIVATE_KEY }}
- name: Submit update for review
if: ${{ !inputs.dry_run && github.ref == 'refs/heads/master' }}
run: pnpm run submit -- --chrome-zip .output/*-chrome.zip
env:
CHROME_API_VERSION: v2
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CHROME_PUBLISHER_ID: ${{ secrets.CHROME_PUBLISHER_ID }}
CHROME_SERVICE_ACCOUNT_CLIENT_EMAIL: ${{ secrets.CHROME_SERVICE_ACCOUNT_CLIENT_EMAIL }}
CHROME_SERVICE_ACCOUNT_PRIVATE_KEY: ${{ secrets.CHROME_SERVICE_ACCOUNT_PRIVATE_KEY }}
25 changes: 25 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Verify extension

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run compile
- run: pnpm run lint
- run: pnpm run test:unit
- run: pnpm run build
- run: pnpm exec playwright install --with-deps chromium
- run: pnpm run test:e2e
35 changes: 35 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# Config files
.webextrc
.webextrc.*

.vite/
.output/
.wxt/
OLD/
playwright-report/
test-results/
64 changes: 38 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,53 @@
![Logo](/release/marquee.png)
## LiuChan Chinese Popup Dictionary
LiuChan (*liú chàng*) is an extension for chrome that allows you to mouse-over Chinese to instantly lookup the dictionary.
![LiuChan](assets/release/marquee.png)

It started out as a port of RikaiChan (and its Rikai derivatives) and has since grown into an extension that has been completely rewritten with greatly improved performance and new features.
# LiuChan Chinese Popup Dictionary

### Current Features
LiuChan (_liú chàng_, 流畅) is a Chrome extension that looks up Chinese text as you move the pointer over it. Version 2 is a Manifest V3 rewrite with Mandarin and Cantonese support.

* Instantaneous translation of Chinese characters and words on mouse over
* Synchronized settings across all your devices
* Text input support
* A handy little notepad that you can open on any page :)
* Theme support
* Fuzzy search! Use chrome's omnibox to search the dictionary using hanzi, pinyin or just plain english
* Dictionaries automatically update to newer versions
## Features

### Usage
- Fast mouse-over lookup on page text, inputs, and textareas
- Simplified and traditional Hanzi
- CC-CEDICT Mandarin definitions with Pinyin, tone numbers, or Zhuyin
- CC-Canto definitions and supplemental Jyutping readings
- Mandarin, Cantonese, or combined dictionary display
- Synchronized display settings, themes, and custom colors
- Text highlighting, speech, clipboard export, and navigation hotkeys
- User-triggered dictionary updates without reinstalling the extension

**Mouse Over Dictionary:** Simply enable the extension and mouse over Chinese on a page!
Click the toolbar icon to enable or disable LiuChan. The options page documents the available popup hotkeys.

**Fuzzy search:** Type 'lc' in the omnibox followed by whatever term you want to look for (Chinese, pinyin or English), the results will show up as suggestions.
## Dictionary updates

### To Do
The extension includes an offline dictionary. In **Settings → Dictionary data**, click **Check for updates** to download the latest CC-CEDICT editor export and the latest published CC-Canto data. Downloads happen only after this button is clicked. New data is validated before it replaces the active dictionary; **Restore bundled data** always returns to the release snapshot.

- [ ] Add Cantonese dictionary
- [ ] Add word lists/Anki support
- [ ] Move fuzzy search to its own input box as opposed to omnibox so that it can be expanded upon (eg. use rich formatting, etc)
Dictionary files are data only. LiuChan does not download or execute remote code.

### Why is it called LiuChan?
## Development

流畅 (*liú chàng*) stands for fluency in a language. I kept -Chan as an homage to its origin.
Requirements: Node.js 22 and pnpm 10.11.0.

### Troubleshooting
```sh
pnpm install --frozen-lockfile
pnpm run compile
pnpm run lint
pnpm run test:unit
pnpm run build
pnpm exec playwright install chromium
pnpm run test:e2e
pnpm run zip
```

If anything malfunctions, your best bet is disabling and re-enabling the extension and reloading the page you want it to work on.
Load `.output/chrome-mv3` as an unpacked extension for local testing. The Chrome Store zip is written to `.output/`.

In case a problem persists please open an issue on github and try to describe as clearly as possible how to recreate the problem.
## Data sources and licenses

#### Known issues:
- [CC-CEDICT](https://cc-cedict.org/) — community-maintained Mandarin dictionary published by MDBG, Creative Commons Attribution-ShareAlike 4.0.
- [CC-Canto](https://cccanto.org/download.html) — Cantonese dictionary, copyright Pleco Inc., Creative Commons Attribution-ShareAlike 3.0.
- CC-CEDICT Cantonese Readings — supplemental readings from the CC-Canto download, copyright Pleco Software Incorporated, Creative Commons Attribution-ShareAlike 3.0.

Currently fuzzy search isn't 'fuzzy' enough to match regular vowels with tonemarked ones.
LiuChan source code is licensed under GPL-3.0.

## Troubleshooting

Chrome cannot inject extensions into internal pages such as `chrome://` or into tabs that have not been reloaded since an extension update. Reload the affected page and try again. If a dictionary update fails, the previously working data remains active.
41 changes: 41 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Chrome Web Store release

## Repository setup

Create a protected GitHub environment named `chrome-web-store`, then add these repository or environment secrets:

- `CHROME_EXTENSION_ID`
- `CHROME_PUBLISHER_ID`
- `CHROME_SERVICE_ACCOUNT_CLIENT_EMAIL`
- `CHROME_SERVICE_ACCOUNT_PRIVATE_KEY`

The workflow uses Chrome Web Store API v2 with a service account, so it does
not need a user OAuth client or refresh token:

1. Enable the Chrome Web Store API in a Google Cloud project.
2. Create a service account; it does not need a Google Cloud role for this API.
3. Create a JSON key for it. Store its `client_email` and `private_key` values
in the corresponding GitHub secrets above. Never commit the JSON key.
4. In the Chrome Web Store Developer Dashboard, open **Account** and add the
service account email. Chrome currently permits one service account per
publisher.
5. Copy the publisher ID from **Publisher > Settings** into
`CHROME_PUBLISHER_ID`.

Follow the official [service-account setup](https://developer.chrome.com/docs/webstore/service-accounts) and enable two-step verification on the human developer account. The previously configured `CHROME_CLIENT_ID` and `CHROME_CLIENT_SECRET` are only used by the deprecated v1.1 API and can be removed from GitHub after the v2 dry run succeeds.

## Before submitting

1. Confirm `package.json` has a version greater than the currently published version.
2. Run the complete local verification sequence from the README.
3. Review the generated `.output/chrome-mv3/manifest.json` and zip contents.
4. Update the Store listing so it advertises only the features in the current README.
5. In Privacy practices, disclose that page text is processed locally for lookup and is not collected or transmitted. Dictionary update requests contact `cc-cedict.org` and `cccanto.org` only after a user clicks the update button.
6. Refresh screenshots to show the MV3 popup, Mandarin/Cantonese modes, and options page.
7. Keep the dictionary source and license attribution from the README/options page in the Store description.

## Workflow

Run **Release to Chrome Web Store** from the Actions tab with `dry_run` enabled first. This validates authentication without uploading. After it succeeds, run it from `master` with `dry_run` disabled. The workflow builds a fresh zip, retains it as an artifact, uploads it to the existing listing, and submits it for review.

If automated submission is unavailable, download the workflow artifact and use **Upload New Package** in the Chrome Developer Dashboard.
21 changes: 16 additions & 5 deletions testing/DictTest.html → assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@
<title>Dict Test</title>
</head>
<body>
<script src="dicttest.js"></script>
<script src="../js/fuzzysort.js"></script>
<!--<script src="dicttest.js"></script>-->
<!--<script src="../src/js/lib/fuzzysort.js"></script>-->

</br></br>
<div>
一言既出,駟馬難追
一部二十四史,不知從何說起
一顆老鼠屎壞了一鍋湯
丈二金剛摸不著頭腦
三個臭皮匠,勝過一個諸葛亮
中央人民政府駐香港特別行政區聯絡辦公室
你好
中央人民政<a href="test">府駐香港特別</a>行政區聯絡辦公室
</div>
<p>一部二十四史,不知從何說起
一顆老鼠<b>屎壞了一</b>鍋湯
丈二金<p>剛摸不著頭腦</p>
三個臭皮匠,勝過一個諸葛亮</p>

<button>这只为测试插件</button>
<form>
<table>
<tr>
Expand Down Expand Up @@ -150,13 +159,15 @@
<td>
<textarea rows = "5" cols = "50" name = "description">
这只为测试插件
这只为测试插件
这只为测试插件这 只为测试插件
</textarea>
</td>
</tr>
</table>
</form>

<img src="../images/icon128.png" alt="[与其]临渊羡鱼,不如退而结网。《汉书 · 董仲书传》" title="疾风知劲草,板荡识诚臣。《赐萧蠫》">
<img src="release/icon128.png" alt="[与其]临渊羡鱼,不如退而结网。《汉书 · 董仲书传》" title="疾风知劲草,板荡识诚臣。《赐萧蠫》">

</br></br>

Expand Down Expand Up @@ -193,4 +204,4 @@
欢迎,迎接

</body>
</html>
</html>
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
1 change: 0 additions & 1 deletion dictionary/version.txt

This file was deleted.

92 changes: 92 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// @ts-check

import tseslint from 'typescript-eslint'
import simpleImportSort from 'eslint-plugin-simple-import-sort'
import typescriptSortKeys from 'eslint-plugin-typescript-sort-keys'
import sortKeysFix from 'eslint-plugin-sort-keys-fix'
import sortDestructureKeys from 'eslint-plugin-sort-destructure-keys'
import eslint from '@eslint/js'
import react from 'eslint-plugin-react'

export default tseslint.config({
ignores: ['.output/**', '.wxt/**', 'dist/**', 'OLD/**', 'node_modules/**'],
}, {
files: ['**/*.ts', '**/*.tsx'],
plugins: {
'simple-import-sort': simpleImportSort,
'sort-keys-fix': sortKeysFix,
'typescript-sort-keys': typescriptSortKeys,
'sort-destructure-keys': sortDestructureKeys,
react,
},
extends: [
eslint.configs.recommended,
...tseslint.configs.recommended,
// 'plugin:@typescript-eslint/recommended',
// 'plugin:import/typescript',
// 'plugin:react/recommended',
// 'plugin:react/jsx-runtime',
// 'plugin:react-hooks/recommended',
// 'prettier',
],
rules: {
'no-console': 'warn',
// Typescript es-lint handles this
'no-unused-vars': 'off',
'no-case-declarations': 'off',

/**
* React
*/
'react/prop-types': 'off',
'react/jsx-curly-brace-presence': [
'warn',
{
props: 'never',
},
],

/**
* Typescript
*/
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-member-accessibility': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/ban-types': 'warn',
'@typescript-eslint/no-explicit-any': 'off',

/**
* Sorting
*/
'simple-import-sort/imports': 'warn',
'simple-import-sort/exports': 'warn',
'sort-keys-fix/sort-keys-fix': 'warn',
'typescript-sort-keys/interface': 'warn',
'typescript-sort-keys/string-enum': 'warn',
'react/jsx-sort-props': [
'warn',
{
ignoreCase: true,
reservedFirst: true,
shorthandLast: true,
},
],
'sort-destructure-keys/sort-destructure-keys': [
'warn',
{ caseSensitive: false },
],

/**
* Misc rules
*/
eqeqeq: ['error', 'always'],
},
})
Loading
Loading