diff --git a/.gitignore b/.gitignore
index 504e73b..c975d23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,5 @@ local.properties
# 模型文件体积大,不入库;运行时手动放入 app/src/main/assets/
*.onnx
+~$*.docx
+models/
diff --git a/app/src/main/java/com/k2fsa/sherpa/onnx/LanguageUtil.kt b/app/src/main/java/com/k2fsa/sherpa/onnx/LanguageUtil.kt
new file mode 100644
index 0000000..a389f93
--- /dev/null
+++ b/app/src/main/java/com/k2fsa/sherpa/onnx/LanguageUtil.kt
@@ -0,0 +1,66 @@
+package com.k2fsa.sherpa.onnx
+
+import android.content.Context
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.os.LocaleListCompat
+import java.util.Locale
+
+// ============================================================================
+// LanguageUtil — 应用界面语言(中/英/日 + 跟随系统)
+//
+// 持久化在 SharedPreferences;启动时 apply() 一次;切换走
+// AppCompatDelegate.setApplicationLocales(appcompat 1.6 自带 API<33 回移),
+// 由框架自动重建 Activity。业务数据(声纹内部键、纪要 schema 键、jsonl)
+// 一律不受界面语言影响 —— 语言只影响"显示层"。
+// ============================================================================
+object LanguageUtil {
+ const val MODE_SYSTEM = "system"
+ const val MODE_ZH = "zh"
+ const val MODE_EN = "en"
+ const val MODE_JA = "ja"
+
+ private const val PREFS = "lang"
+ private const val KEY = "mode"
+
+ fun mode(ctx: Context): String =
+ ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
+ .getString(KEY, MODE_SYSTEM) ?: MODE_SYSTEM
+
+ fun apply(ctx: Context) {
+ val locales = when (mode(ctx)) {
+ MODE_ZH -> LocaleListCompat.forLanguageTags("zh")
+ MODE_EN -> LocaleListCompat.forLanguageTags("en")
+ MODE_JA -> LocaleListCompat.forLanguageTags("ja")
+ else -> LocaleListCompat.getEmptyLocaleList() // 跟随系统
+ }
+ // 值相同则为 no-op,不会触发重建
+ if (AppCompatDelegate.getApplicationLocales() != locales) {
+ AppCompatDelegate.setApplicationLocales(locales)
+ }
+ }
+
+ fun set(ctx: Context, m: String) {
+ ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
+ .edit().putString(KEY, m).apply()
+ // setApplicationLocales 会自动 recreate;system 模式下清空后回退
+ // 到当前显示语言,避免"空 locale 不生效导致不重建"
+ val target = if (m == MODE_SYSTEM) currentFallback() else m
+ val locales = when (target) {
+ MODE_JA -> LocaleListCompat.forLanguageTags("ja")
+ MODE_EN -> LocaleListCompat.forLanguageTags("en")
+ else -> LocaleListCompat.forLanguageTags("zh")
+ }
+ AppCompatDelegate.setApplicationLocales(locales)
+ if (m == MODE_SYSTEM) {
+ // 再切回真正的跟随系统(下一次启动生效)
+ ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
+ .edit().putString(KEY, MODE_SYSTEM).apply()
+ AppCompatDelegate.setApplicationLocales(
+ LocaleListCompat.forLanguageTags(currentFallback())
+ )
+ }
+ }
+
+ private fun currentFallback(): String =
+ Locale.getDefault().language.takeIf { it == "ja" || it == "en" } ?: "zh"
+}
diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml
new file mode 100644
index 0000000..4180b40
--- /dev/null
+++ b/app/src/main/res/values-en/strings.xml
@@ -0,0 +1,75 @@
+
+ Tap Start to begin live transcription.\n\nSource code and all models are open resources — see github.com/k2fsa/sherpa-onnx.\nTwo-pass recognition · speaker ID · offline meeting minutes
+ Start
+ Stop
+ Pause
+ Resume
+ OK
+ Cancel
+ Minutes
+ Cancel
+ Copy
+ History
+ Back to live
+ Language
+ UI language
+ Follow system
+ Tap “Minutes” to summarize this meeting offline. The latest minutes appear here.
+
+ Loading 1st-pass model (nemotron-3.5)…
+ Loading 2nd-pass model (funasr-nano)…
+ Loading speaker model…
+ Models ready.
+ Tap Start to begin
+ Speaker ID enabled
+ ⚠ Speaker ID off: model %1$s missing (adb push it to %2$s)
+ Model loading failed: %1$s\n\nAll models live under %2$s — push them with adb
+
+ Identified %1$d
+ Unknown
+
+ Name speaker “%1$s”
+ Rename “%1$s” to…
+ Real name for “%1$s”, e.g. Alice
+ Name can’t be “S+digits” or a reserved display name
+ Renamed to “%1$s”; adjacent lines of the same speaker merged
+ Speaker not settled yet — finish a full sentence first
+
+ Past meetings
+ Recording — end this session before browsing history
+ No past meetings to show
+ %1$s · %2$d lines · %3$s%4$s
+ · has minutes
+ History: %1$s (read-only)
+
+ Choose a meeting to summarize
+ [Current meeting] %1$d lines · %2$s
+ · has minutes (continue)
+ Nothing to summarize yet — record a meeting first
+ This meeting has no settled lines
+ Minutes in progress — tap again to cancel
+ This device ABI lacks the summarizer runtime (arm64-v8a only)
+ Summarizer model missing. Run:\n%1$s
+ Loading summarizer model (%1$s)…
+ notes
+ drafting
+ Running %1$s: %2$d/%3$d
+ Model ready, resuming from checkpoint (%1$d/%2$d lines done)…
+ Model ready, %1$d lines — building notes…
+ Minutes saved (%1$s, %2$d chunks%3$s): %4$s
+ , %1$d chunks failed
+ Cancelled (previous minutes kept)
+ Cancelled automatically: %1$s (previous minutes kept)
+ Failed: %1$s
+ Cancelled
+ Cancelled automatically
+ Summarizing — transcription continues uninterrupted
+ Minutes copied to clipboard
+
+ Last minutes: %1$s (will be overwritten by a new run)
+ Meeting “%1$s” has no minutes yet — tap “Minutes” to finish it (incremental)
+
+ Session active — end recording before switching language
+ Copied to clipboard
+ Cancelling…
+
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000..50ac339
--- /dev/null
+++ b/app/src/main/res/values-ja/strings.xml
@@ -0,0 +1,75 @@
+
+ 「開始」を押すとリアルタイム文字起こしを開始します。\n\nソースコードとモデルはすべて公開されています: github.com/k2fsa/sherpa-onnx\n2パス認識 · 話者識別 · オフライン議事録
+ 開始
+ 停止
+ 一時停止
+ 続行
+ OK
+ キャンセル
+ 議事録生成
+ 生成を中止
+ 議事録コピー
+ 過去の会議
+ 現在に戻る
+ 言語
+ 表示言語
+ システムに追従
+ 「議事録生成」を押すとこの会議をオフラインで要約します。結果と前回の議事録はここに表示されます
+
+ 第1パスモデル(nemotron-3.5)を読み込み中…
+ 第2パスモデル(funasr-nano)を読み込み中…
+ 声紋モデルを読み込み中…
+ モデルの準備が完了しました。
+ 「開始」を押して認識してください
+ 話者識別は有効です
+ ⚠ 話者識別は無効:声紋モデル %1$s がありません(%2$s へ adb push してください)
+ モデル読み込みに失敗:%1$s\n\nモデルはすべて %2$s に配置 — adb push で配信してください
+
+ 識別済み %1$d
+ 未識別
+
+ 話者「%1$s」に名前を付ける
+ 「%1$s」を次の名前に変更…
+ 「%1$s」の本名(例:田中)
+ 名前は「S+数字」やシステム予約名は使えません
+ 「%1$s」に改名し、隣接する同一話者の行を結合しました
+ この発話の話者はまだ確定していません。一文話し終えてから命名してください
+
+ 過去の会議
+ 録音中です。今回の会議を終了してから履歴を閲覧できます
+ 閲覧できる過去の会議がありません
+ %1$s · %2$d 発 · %3$s%4$s
+ · 議事録あり
+ 履歴:%1$s(読み取り専用)
+
+ 要約する会議を選択
+ 【現在の会議】%1$d 発 · %2$s
+ · 議事録あり(続きから)
+ 要約できる会議がありません。先に録音してください
+ この会議に確定した発話がありません
+ 議事録を生成中です。もう一度押すと中止できます
+ この端末のABIには要約ランタイムが含まれていません(arm64-v8aのみ)
+ 要約モデルがありません。実行してください:\n%1$s
+ 要約モデル(%1$s)を読み込み中…
+ メモ更新
+ 清書
+ %1$s実行中: %2$d/%3$d
+ モデル準備完了、前回の中断点から続行(%1$d/%2$d 消化済み)…
+ モデル準備完了、全 %1$d 発、メモ構築を開始…
+ 議事録を保存しました(%1$s、%2$d ブロック%3$s):%4$s
+ 、%1$d ブロック解析失敗
+ 中止しました(前回の議事録は保持)
+ 自動キャンセル:%1$s(前回の議事録は保持)
+ 生成失敗:%1$s
+ 中止しました
+ 自動キャンセルしました
+ 議事録生成中でも文字起こしは停止しません
+ 議事録をクリップボードにコピーしました
+
+ 前回の議事録:%1$s(新規生成すると上書きされます)
+ 前回の会議「%1$s」は議事録未生成です。「議事録生成」で続きを生成できます(増分、新規発話のみ処理)
+
+ セッション終了後に言語を切り替えられます
+ クリップボードにコピーしました
+ 中止中…
+
diff --git a/docs/项目阶段性总结_2026-09-11.docx b/docs/项目阶段性总结_2026-09-11.docx
new file mode 100644
index 0000000..9021136
Binary files /dev/null and b/docs/项目阶段性总结_2026-09-11.docx differ