Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# ApkStart

[Interface translations / Traduções da interface](docs/I18N.md) · 简体中文 / English / Português (Brasil)

<p align="center"><img src="docs/images/logo.png" alt="ApkStart Logo" width="160" /></p>

[![CI](https://github.com/RoutineMiku/apkstart/actions/workflows/ci.yml/badge.svg)](https://github.com/RoutineMiku/apkstart/actions/workflows/ci.yml)
Expand Down
15 changes: 15 additions & 0 deletions build/electron-builder-test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');

// Lightweight test installer: reuse the runtime already in the user's data directory.
// A new installation downloads the runtime on demand, as the portable build does.
const config = yaml.load(fs.readFileSync(path.join(__dirname, '../electron-builder.yml'), 'utf8'));
module.exports = {
...config,
directories: { ...config.directories, output: 'dist/i18n-test' },
extraMetadata: { version: '0.4.2-i18n.1' },
extraResources: [],
publish: null,
};
18 changes: 14 additions & 4 deletions build/installer.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,23 @@
; 用户装完发现桌面上什么都没有。这里用 NSIS 原生的 CreateShortcut 再写一次:
; $DESKTOP 走 SHGetFolderPath,能正确跟随桌面的重定向路径;写成幂等操作,
; 和 electron-builder 的重复执行也只是覆盖同一个 .lnk。
LangString ApkStartCreateShortcuts 2052 "正在创建桌面快捷方式…"
LangString ApkStartCreateShortcuts 1033 "Creating desktop shortcuts…"
LangString ApkStartCreateShortcuts 1046 "Criando atalhos na área de trabalho…"
LangString ApkStartShortcutDescription 2052 "ApkStart — 在电脑上直接运行安卓应用"
LangString ApkStartShortcutDescription 1033 "ApkStart — Run Android apps on your PC"
LangString ApkStartShortcutDescription 1046 "ApkStart — Execute apps Android no seu computador"
LangString un.ApkStartStopProcesses 2052 "正在结束 ApkStart 残留进程…"
LangString un.ApkStartStopProcesses 1033 "Stopping remaining ApkStart processes…"
LangString un.ApkStartStopProcesses 1046 "Encerrando os processos restantes do ApkStart…"

!macro customInstall
DetailPrint "正在创建桌面快捷方式…"
DetailPrint "$(ApkStartCreateShortcuts)"
CreateShortcut "$DESKTOP\ApkStart.lnk" "$INSTDIR\ApkStart.exe" "" "$INSTDIR\ApkStart.exe" 0 \
SW_SHOWNORMAL "" "ApkStart — 在电脑上直接运行安卓应用"
SW_SHOWNORMAL "" "$(ApkStartShortcutDescription)"
CreateDirectory "$SMPROGRAMS\ApkStart"
CreateShortcut "$SMPROGRAMS\ApkStart\ApkStart.lnk" "$INSTDIR\ApkStart.exe" "" "$INSTDIR\ApkStart.exe" 0 \
SW_SHOWNORMAL "" "ApkStart — 在电脑上直接运行安卓应用"
SW_SHOWNORMAL "" "$(ApkStartShortcutDescription)"
!macroend

!macro customUnInstall
Expand All @@ -26,7 +36,7 @@
Delete "$SMPROGRAMS\ApkStart\ApkStart.lnk"
RMDir "$SMPROGRAMS\ApkStart"

DetailPrint "正在结束 ApkStart 残留进程…"
DetailPrint "$(un.ApkStartStopProcesses)"
; /F 强杀 /T 连带子进程。进程不存在时 taskkill 返回 128,忽略即可。
nsExec::Exec 'taskkill.exe /F /T /IM ApkStart.exe'
nsExec::Exec 'taskkill.exe /F /T /IM qemu-system-x86_64.exe'
Expand Down
2 changes: 1 addition & 1 deletion build/pack.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ fs.rmSync(path.join(OUT, 'resources', 'default_app.asar'), { force: true });

// 1.1 语言包瘦身:Electron 自带 55 种语言,合计 41MB,我们只用中文界面。
// 只留 zh-CN / zh-TW / en-US,其余删掉(-39MB,等于整包小 15%)。
const KEEP_LOCALES = new Set(['zh-CN.pak', 'zh-TW.pak', 'en-US.pak']);
const KEEP_LOCALES = new Set(['zh-CN.pak', 'zh-TW.pak', 'en-US.pak', 'pt-BR.pak']);
let localeSaved = 0;
try {
const locDir = path.join(OUT, 'locales');
Expand Down
56 changes: 56 additions & 0 deletions docs/I18N.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Interface translations

The interface supports Simplified Chinese (`zh-CN`), Brazilian Portuguese (`pt-BR`),
and English (`en`). Settings → Language saves the selection immediately and reloads
the main window. Existing Android app windows pick up the new language when reopened.
Automatic mode follows the Windows locale, using English for unsupported languages.

Messages live in `src/locales/*.json`. Keep the same keys and numbered placeholders
in all three catalogs. Existing `mNNNN` keys identify extracted messages and must not
be renumbered. Use descriptive keys for new messages. Some messages contain trusted
interface HTML; preserve their markup and escape user content before interpolation.
App names, package names, file paths, release notes and raw runtime logs are not
translated. Some diagnostics emitted by the underlying runtime still use Chinese.

`src/core/i18n.js` handles locale selection and main-process messages. The renderer
loads its catalog through the allowlisted `i18n:get` IPC channel. Static HTML uses
`data-i18n` and `data-i18n-title/placeholder/alt/aria-label`; JavaScript uses `tr()`.
No runtime dependency is added for internationalization.

## Validation

```powershell
npm test
npm run test:i18n
npm run test:i18n:ui
```

The UI test uses mocked APIs in an isolated Electron window. It checks language
switching, persistence across reloads, app-launch feedback and settings overflow.
Screenshots are written to `test/out/i18n/`. It does not install APKs or alter the
user's installation. Real Android app startup still needs a manual test.

## Local test installer

```powershell
npm install
npm run build:exe:test
```

Output: `dist/i18n-test/ApkStart-0.4.2-i18n.1-Setup.exe` (Windows x64).
This unsigned, unpublished test build uses the same application ID and data paths
as ApkStart. It omits the bundled Android runtime and reuses an existing runtime in
the user data directory; missing runtime components download on demand. It must be
installed in the same location as the current installation to replace that copy.
The installer language and interface language can be selected independently.

## Teste em português

1. Feche o ApkStart pela opção Sair no ícone da bandeja do Windows.
2. Execute o instalador de teste e escolha a mesma pasta da instalação atual.
3. Abra Configurações → Idioma e selecione Português (Brasil).
4. Confira as telas, abra um app Android já instalado e teste a opção English.

O instalador mantém o mesmo identificador e os mesmos caminhos de dados do projeto.
Ele não inclui a imagem Android completa. Logs brutos e alguns diagnósticos internos
ainda podem aparecer em chinês. A compilação é local e não publica uma versão ou PR.
7 changes: 6 additions & 1 deletion electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ electronLanguages:
- zh-CN
- zh-TW
- en-US
- pt-BR

# 完整运行时(emulator + platform-tools + 系统镜像),由 build/prepare-runtime.js 生成。
# 打进包里之后首次启动不再需要任何下载。
Expand All @@ -51,7 +52,11 @@ nsis:
oneClick: false
perMachine: false
allowToChangeInstallationDirectory: true
displayLanguageSelector: false
displayLanguageSelector: true
installerLanguages:
- zh_CN
- en_US
- pt_BR
# always:即使桌面上已有同名 .lnk 也重建一次。
# 默认的 true 只在「不存在」时才创建,某些机器上(安全软件拦截、桌面被
# OneDrive 重定向、旧版残留了指向已删目录的坏快捷方式)会出现装完没图标,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@
"selftest": "node scripts/selftest.js",
"assoc:on": "node scripts/assoc.js on",
"assoc:off": "node scripts/assoc.js off",
"test:boot": "node test/boot-test.js"
"test:boot": "node test/boot-test.js",
"test:i18n": "node test/i18n-test.js",
"test:i18n:ui": "electron test/i18n-ui-test.js",
"build:exe:test": "electron-builder --win nsis --x64 --config build/electron-builder-test.cjs --publish never"
},
"devDependencies": {
"electron": "^33.4.11",
Expand Down
10 changes: 7 additions & 3 deletions src/core/doctor.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
const { t } = require('./i18n');
/**
* 设备自适应 / 自动诊断(零配置底座)
*
Expand Down Expand Up @@ -435,9 +436,12 @@ function diagnose(lines) {

/** 人类可读的一行摘要,放在设置页让高级用户知道自动选了什么 */
function describeProfile(p) {
return `自动:${(p.memoryMB / 1024).toFixed(1)}GB 内存 · ${p.cores} 核 · ${p.gpu} 渲染 · `
+ `${p.accelOk ? '硬件加速' : '软件模拟'} · ${p.snapshot ? '按需快照' : '冷启动'}`
+ `${p.lite ? ' · 轻量容器' : ''} · 端口 ${p.port}`;
return t('profile.summary', [
(p.memoryMB / 1024).toFixed(1), p.cores, p.gpu,
p.accelOk ? t('profile.accelerated') : t('m0019'),
p.snapshot ? t('profile.snapshot') : t('profile.coldBoot'),
p.lite ? t('profile.lite') : '', p.port,
]);
}

/** 导出诊断报告(纯文本,便于发邮件/贴 issue) */
Expand Down
36 changes: 36 additions & 0 deletions src/core/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';

const catalogs = {
'zh-CN': require('../locales/zh-CN.json'),
en: require('../locales/en.json'),
'pt-BR': require('../locales/pt-BR.json'),
};
let locale = 'zh-CN';

function resolveLocale(preference, systemLocale = 'en') {
if (Object.hasOwn(catalogs, preference)) return preference;
const system = String(systemLocale).toLowerCase().replace(/_/g, '-');
if (system === 'pt' || system.startsWith('pt-')) return 'pt-BR';
if (system === 'zh' || system.startsWith('zh-')) return 'zh-CN';
return 'en';
}

function format(message, values = []) {
return message.replace(/\{(\d+)\}/g, (match, index) => (
index < values.length ? String(values[index]) : match
));
}

function getCatalog(language) {
return catalogs[language] || catalogs.en;
}

function setLocale(language) {
locale = resolveLocale(language);
}

function t(key, values) {
return format(getCatalog(locale)[key] ?? catalogs.en[key] ?? catalogs['zh-CN'][key] ?? key, values);
}

module.exports = { resolveLocale, format, getCatalog, setLocale, t };
1 change: 1 addition & 0 deletions src/core/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const APPS_FILE = path.join(DATA_DIR, 'apps.json');
const SETTINGS_FILE = path.join(DATA_DIR, 'settings.json');

const DEFAULT_SETTINGS = {
language: 'auto',
adbPath: '',
device: '127.0.0.1:5555',
engine: {
Expand Down
Loading