Android 알림 글 · 채널 이름을 화면 언어로 (57)

- 앱이 고른 언어라 Android 의 values-xx 로는 맞출 수 없어, Dart (AndroidKeepAlive) 가 시작할 때와 화면 언어가 바뀔 때
  번역된 글 8개 (진행 알림 제목, "작업 멈춤" 알림, 채널 2개의 이름 · 설명) 를 보낸다.
- Kotlin 은 받은 글을 SharedPreferences 에 저장해 앱이 꺼진 뒤 서비스만 떠도 마지막 언어로 쓰고,
  이미 만든 채널은 같은 ID 로 다시 만들어 이름 · 설명만 바꾼다. 받은 적이 없으면 한국어.
- notification_texts_test: Kotlin 기본값과 Dart 의 한국어 글이 같은지, ja 로 바꾸면 모두 번역되는지.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음4
RyuJongJin 2026-10-10 00:45:52 +09:00
parent abeb3d8999
commit a4afdda5c6
7 changed files with 145 additions and 17 deletions

View File

@ -85,17 +85,65 @@ class KeepAliveService : Service() {
const val DONE_CHANNEL_ID = "done" const val DONE_CHANNEL_ID = "done"
/// 57: 알림 글 · 채널 이름을 앱 화면 언어로 (앱이 고른 언어라 Android 의 values-xx 로는 맞출 수 없다).
/// 앱 (Dart) 이 언어를 바꿀 때마다 보내 오고, 다음에 켤 때도 쓰게 저장해 둔다. 없으면 한국어.
private const val PREFS = "jj_notify_texts"
private val defaults = mapOf(
"working" to "JJ_MKVMaker 작업 중",
"stoppedTitle" to "JJ_MKVMaker 작업이 멈췄습니다",
"stoppedText" to "앱을 닫아 진행 중이던 작업이 멈췄습니다. 닫아도 계속하려면 환경 설정에서 '백그라운드로 실행' 을 켜세요.",
"stoppedLong" to "앱을 닫아 진행 중이던 작업 (MKV · 다운로드 · 동기화) 이 멈췄습니다. 앱을 닫아도 계속하려면 환경 설정 > Rsync > '백그라운드로 실행' 을 켜세요.",
"jobsChannel" to "작업 진행",
"jobsChannelDesc" to "다운로드 · MKV 만들기 · AI 자막 · 동기화 진행 상황",
"doneChannel" to "끝난 작업",
"doneChannelDesc" to "AI 해상도 올리기 등 오래 걸린 작업이 끝났을 때",
)
private fun t(context: Context, key: String): String =
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(key, null)?.takeIf { it.isNotEmpty() }
?: defaults.getValue(key)
/// 앱이 보낸 글을 저장하고, 이미 만든 채널은 이름 · 설명을 바꾼다 (같은 ID 로 다시 만들면 이름만 바뀐다)
fun setTexts(context: Context, texts: Map<String, String>) {
val e = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
for ((k, v) in texts) if (k in defaults) e.putString(k, v)
e.apply()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (nm.getNotificationChannel(CHANNEL_ID) != null) createJobsChannel(context, nm, texts)
if (nm.getNotificationChannel(DONE_CHANNEL_ID) != null) createDoneChannel(context, nm, texts)
}
}
private fun pick(context: Context, texts: Map<String, String>?, key: String) =
texts?.get(key)?.takeIf { it.isNotEmpty() } ?: t(context, key)
private fun createJobsChannel(context: Context, nm: NotificationManager, texts: Map<String, String>? = null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
nm.createNotificationChannel(
NotificationChannel(CHANNEL_ID, pick(context, texts, "jobsChannel"), NotificationManager.IMPORTANCE_LOW).apply {
description = pick(context, texts, "jobsChannelDesc")
setShowBadge(false)
}
)
}
private fun createDoneChannel(context: Context, nm: NotificationManager, texts: Map<String, String>? = null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
nm.createNotificationChannel(
NotificationChannel(DONE_CHANNEL_ID, pick(context, texts, "doneChannel"), NotificationManager.IMPORTANCE_DEFAULT).apply {
description = pick(context, texts, "doneChannelDesc")
setSound(null, null)
}
)
}
/// 157: 끝난 작업 알림 (예: "4장 저장: 폴더"). 누르면 작업 현황으로. /// 157: 끝난 작업 알림 (예: "4장 저장: 폴더"). 누르면 작업 현황으로.
/// 진행 알림 채널 (조용히, 낮은 중요도) 과 따로 두어 알림 창의 "조용한 알림" 칸에 묻히지 않게 (보통 중요도, 소리 없음) /// 진행 알림 채널 (조용히, 낮은 중요도) 과 따로 두어 알림 창의 "조용한 알림" 칸에 묻히지 않게 (보통 중요도, 소리 없음)
fun notifyDone(context: Context, title: String, text: String) { fun notifyDone(context: Context, title: String, text: String) {
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && nm.getNotificationChannel(DONE_CHANNEL_ID) == null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && nm.getNotificationChannel(DONE_CHANNEL_ID) == null) {
nm.createNotificationChannel( createDoneChannel(context, nm)
NotificationChannel(DONE_CHANNEL_ID, "끝난 작업", NotificationManager.IMPORTANCE_DEFAULT).apply {
description = "AI 해상도 올리기 등 오래 걸린 작업이 끝났을 때"
setSound(null, null)
}
)
} }
val open = PendingIntent.getActivity( val open = PendingIntent.getActivity(
context, 3, context, 3,
@ -134,10 +182,9 @@ class KeepAliveService : Service() {
@Suppress("DEPRECATION") Notification.Builder(context) @Suppress("DEPRECATION") Notification.Builder(context)
} }
b.setSmallIcon(android.R.drawable.stat_notify_error) b.setSmallIcon(android.R.drawable.stat_notify_error)
.setContentTitle("JJ_MKVMaker 작업이 멈췄습니다") .setContentTitle(t(context, "stoppedTitle"))
.setContentText("앱을 닫아 진행 중이던 작업이 멈췄습니다. 닫아도 계속하려면 환경 설정에서 '백그라운드로 실행' 을 켜세요.") .setContentText(t(context, "stoppedText"))
.setStyle(Notification.BigTextStyle().bigText( .setStyle(Notification.BigTextStyle().bigText(t(context, "stoppedLong")))
"앱을 닫아 진행 중이던 작업 (MKV · 다운로드 · 동기화) 이 멈췄습니다. 앱을 닫아도 계속하려면 환경 설정 > Rsync > '백그라운드로 실행' 을 켜세요."))
.setContentIntent(open) .setContentIntent(open)
.setAutoCancel(true) .setAutoCancel(true)
nm.notify(STOPPED_ID, b.build()) nm.notify(STOPPED_ID, b.build())
@ -158,12 +205,7 @@ class KeepAliveService : Service() {
private fun notification(context: Context, text: String, progress: Int, icon: String): Notification { private fun notification(context: Context, text: String, progress: Int, icon: String): Notification {
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && nm.getNotificationChannel(CHANNEL_ID) == null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && nm.getNotificationChannel(CHANNEL_ID) == null) {
nm.createNotificationChannel( createJobsChannel(context, nm)
NotificationChannel(CHANNEL_ID, "작업 진행", NotificationManager.IMPORTANCE_LOW).apply {
description = "다운로드 · MKV 만들기 · AI 자막 · 동기화 진행 상황"
setShowBadge(false)
}
)
} }
val open = PendingIntent.getActivity( val open = PendingIntent.getActivity(
context, 0, context, 0,
@ -180,7 +222,7 @@ class KeepAliveService : Service() {
} }
// 다운로드 중이면 내려받기 아이콘, 그 밖 (실시간 동기화 · MKV · AI 자막) 은 좌우로 오가는 화살표 // 다운로드 중이면 내려받기 아이콘, 그 밖 (실시간 동기화 · MKV · AI 자막) 은 좌우로 오가는 화살표
b.setSmallIcon(if (icon == "download") android.R.drawable.stat_sys_download else R.drawable.ic_stat_sync_anim) b.setSmallIcon(if (icon == "download") android.R.drawable.stat_sys_download else R.drawable.ic_stat_sync_anim)
.setContentTitle("JJ_MKVMaker 작업 중") .setContentTitle(t(context, "working"))
.setContentText(text) .setContentText(text)
.setContentIntent(open) .setContentIntent(open)
.setOngoing(true) .setOngoing(true)

View File

@ -170,6 +170,13 @@ class MainActivity : FlutterActivity() {
call.argument<String>("icon") ?: "") call.argument<String>("icon") ?: "")
result.success(null) result.success(null)
} }
"notificationTexts" -> {
// 57: 알림 글 · 채널 이름 (앱 화면 언어)
val m = call.arguments as? Map<*, *> ?: emptyMap<Any, Any>()
KeepAliveService.setTexts(applicationContext,
m.entries.mapNotNull { (k, v) -> if (k is String && v is String) k to v else null }.toMap())
result.success(null)
}
"notifyDone" -> { "notifyDone" -> {
KeepAliveService.notifyDone(applicationContext, call.argument<String>("title") ?: "", KeepAliveService.notifyDone(applicationContext, call.argument<String>("title") ?: "",
call.argument<String>("text") ?: "") call.argument<String>("text") ?: "")

View File

@ -97,6 +97,7 @@
"AI 해상도 올리기": "AI upscaling", "AI 해상도 올리기": "AI upscaling",
"AI 해상도 올리기 (모든 장)": "AI upscaling (all pages)", "AI 해상도 올리기 (모든 장)": "AI upscaling (all pages)",
"AI 해상도 올리기 (이 장)": "AI upscaling (this page)", "AI 해상도 올리기 (이 장)": "AI upscaling (this page)",
"AI 해상도 올리기 등 오래 걸린 작업이 끝났을 때": "When a long task such as AI upscaling finishes",
"API 키": "API key", "API 키": "API key",
"API 키 (선택)": "API key (optional)", "API 키 (선택)": "API key (optional)",
"API 키 (필수, 무료)": "API key (required, free)", "API 키 (필수, 무료)": "API key (required, free)",
@ -134,6 +135,8 @@
"JJ_MKVMaker - 다운로드 {0}개": "JJ_MKVMaker - {0} downloads", "JJ_MKVMaker - 다운로드 {0}개": "JJ_MKVMaker - {0} downloads",
"JJ_MKVMaker 로 자막 만들기": "Make subtitles with JJ_MKVMaker", "JJ_MKVMaker 로 자막 만들기": "Make subtitles with JJ_MKVMaker",
"JJ_MKVMaker 로 재생": "Play with JJ_MKVMaker", "JJ_MKVMaker 로 재생": "Play with JJ_MKVMaker",
"JJ_MKVMaker 작업 중": "JJ_MKVMaker is working",
"JJ_MKVMaker 작업이 멈췄습니다": "JJ_MKVMaker tasks stopped",
"JJ_MKVMaker_즐겨찾기.html": "JJ_MKVMaker_bookmarks.html", "JJ_MKVMaker_즐겨찾기.html": "JJ_MKVMaker_bookmarks.html",
"JPG 품질 {0}": "JPG quality {0}", "JPG 품질 {0}": "JPG quality {0}",
"JavaScript 사용": "Enable JavaScript", "JavaScript 사용": "Enable JavaScript",
@ -500,6 +503,7 @@
"끝: {0}": "End: {0}", "끝: {0}": "End: {0}",
"끝나지 못했습니다: {0}": "Did not finish: {0}", "끝나지 못했습니다: {0}": "Did not finish: {0}",
"끝난 뒤 원본의 빈 폴더를 지웁니다 (find 원본/ -type d -empty -delete). 원본 폴더는:": "Afterwards, empty folders in the source are deleted (find source/ -type d -empty -delete). The source folder:", "끝난 뒤 원본의 빈 폴더를 지웁니다 (find 원본/ -type d -empty -delete). 원본 폴더는:": "Afterwards, empty folders in the source are deleted (find source/ -type d -empty -delete). The source folder:",
"끝난 작업": "Finished tasks",
"끝내기": "Exit", "끝내기": "Exit",
"끝내기 (작업 멈춤)": "Exit (stop tasks)", "끝내기 (작업 멈춤)": "Exit (stop tasks)",
"끝에서 처음으로 돌기": "Wrap around at the end", "끝에서 처음으로 돌기": "Wrap around at the end",
@ -596,6 +600,7 @@
"다운로드 {0}": "Downloads {0}", "다운로드 {0}": "Downloads {0}",
"다운로드 {0}개": "{0} downloads", "다운로드 {0}개": "{0} downloads",
"다운로드 {0}개{1}": "{0} downloads{1}", "다운로드 {0}개{1}": "{0} downloads{1}",
"다운로드 · MKV 만들기 · AI 자막 · 동기화 진행 상황": "Progress of downloads · MKV creation · AI subtitles · sync",
"다운로드 · 변환도 모두 종료 (묻지 않음)": "Also quit downloads · conversions (don't ask)", "다운로드 · 변환도 모두 종료 (묻지 않음)": "Also quit downloads · conversions (don't ask)",
"다운로드 목록": "Download list", "다운로드 목록": "Download list",
"다운로드 목록 (지금 여기)": "Download list (you are here)", "다운로드 목록 (지금 여기)": "Download list (you are here)",
@ -1137,6 +1142,8 @@
"앱에 들어 있는 것 (기본)": "Built into the app (default)", "앱에 들어 있는 것 (기본)": "Built into the app (default)",
"앱을 다시 설치하자 Android 자동 백업 (Google 백업) 이 이전 설정을 되살렸습니다.\n· 동영상 목록 {0}개\n· WebDAV 서버: {1}": "After reinstalling the app, Android Auto Backup (Google backup) restored your previous settings.\n· {0} videos in the list\n· WebDAV servers: {1}", "앱을 다시 설치하자 Android 자동 백업 (Google 백업) 이 이전 설정을 되살렸습니다.\n· 동영상 목록 {0}개\n· WebDAV 서버: {1}": "After reinstalling the app, Android Auto Backup (Google backup) restored your previous settings.\n· {0} videos in the list\n· WebDAV servers: {1}",
"앱을 다시 켤 때 동기화": "Sync when the app restarts", "앱을 다시 켤 때 동기화": "Sync when the app restarts",
"앱을 닫아 진행 중이던 작업 (MKV · 다운로드 · 동기화) 이 멈췄습니다. 앱을 닫아도 계속하려면 환경 설정 > Rsync > '백그라운드로 실행' 을 켜세요.": "Closing the app stopped the running tasks (MKV · downloads · sync). To keep them going after closing the app, turn on Settings > Rsync > 'Run in background'.",
"앱을 닫아 진행 중이던 작업이 멈췄습니다. 닫아도 계속하려면 환경 설정에서 '백그라운드로 실행' 을 켜세요.": "Closing the app stopped the running tasks. To keep them going after closing, turn on 'Run in background' in Settings.",
"앱을 새로 설치해 설정이 없습니다. 저장해 둔 v{0} 의 설정 (동영상 목록 · 즐겨찾기 포함) 을 되살릴까요?\n(자막 API 키 · 비밀번호는 저장하지 않았으니 다시 넣어 주세요)": "The app was newly installed and has no settings. Restore the saved settings of v{0} (including the video list and bookmarks)?\n(Subtitle API keys and passwords were not saved - please enter them again)", "앱을 새로 설치해 설정이 없습니다. 저장해 둔 v{0} 의 설정 (동영상 목록 · 즐겨찾기 포함) 을 되살릴까요?\n(자막 API 키 · 비밀번호는 저장하지 않았으니 다시 넣어 주세요)": "The app was newly installed and has no settings. Restore the saved settings of v{0} (including the video list and bookmarks)?\n(Subtitle API keys and passwords were not saved - please enter them again)",
"앱을 지우면 다음은 되살릴 수 없습니다:": "Removing the app loses the following for good:", "앱을 지우면 다음은 되살릴 수 없습니다:": "Removing the app loses the following for good:",
"앱이 켜져 있는 동안 늘 동기화합니다.": "Always syncs while the app is running.", "앱이 켜져 있는 동안 늘 동기화합니다.": "Always syncs while the app is running.",
@ -1471,6 +1478,7 @@
"작업 임시 파일": "Job temporary files", "작업 임시 파일": "Job temporary files",
"작업 중": "Working", "작업 중": "Working",
"작업 중지: {0}{1}": "Job stopped: {0}{1}", "작업 중지: {0}{1}": "Job stopped: {0}{1}",
"작업 진행": "Task progress",
"작업 취소": "Cancel job", "작업 취소": "Cancel job",
"작업 현황": "Job status", "작업 현황": "Job status",
"작업 현황 닫기": "Close job status", "작업 현황 닫기": "Close job status",

View File

@ -97,6 +97,7 @@
"AI 해상도 올리기": "AI 高解像度化", "AI 해상도 올리기": "AI 高解像度化",
"AI 해상도 올리기 (모든 장)": "AI 高解像度化 (全ページ)", "AI 해상도 올리기 (모든 장)": "AI 高解像度化 (全ページ)",
"AI 해상도 올리기 (이 장)": "AI 高解像度化 (このページ)", "AI 해상도 올리기 (이 장)": "AI 高解像度化 (このページ)",
"AI 해상도 올리기 등 오래 걸린 작업이 끝났을 때": "AI 高解像度化など時間のかかる作業が終わったとき",
"API 키": "API キー", "API 키": "API キー",
"API 키 (선택)": "API キー (任意)", "API 키 (선택)": "API キー (任意)",
"API 키 (필수, 무료)": "API キー(必須・無料)", "API 키 (필수, 무료)": "API キー(必須・無料)",
@ -134,6 +135,8 @@
"JJ_MKVMaker - 다운로드 {0}개": "JJ_MKVMaker - ダウンロード {0}個", "JJ_MKVMaker - 다운로드 {0}개": "JJ_MKVMaker - ダウンロード {0}個",
"JJ_MKVMaker 로 자막 만들기": "JJ_MKVMaker で字幕を作成", "JJ_MKVMaker 로 자막 만들기": "JJ_MKVMaker で字幕を作成",
"JJ_MKVMaker 로 재생": "JJ_MKVMaker で再生", "JJ_MKVMaker 로 재생": "JJ_MKVMaker で再生",
"JJ_MKVMaker 작업 중": "JJ_MKVMaker 作業中",
"JJ_MKVMaker 작업이 멈췄습니다": "JJ_MKVMaker の作業が停止しました",
"JJ_MKVMaker_즐겨찾기.html": "JJ_MKVMaker_ブックマーク.html", "JJ_MKVMaker_즐겨찾기.html": "JJ_MKVMaker_ブックマーク.html",
"JPG 품질 {0}": "JPG 品質 {0}", "JPG 품질 {0}": "JPG 品質 {0}",
"JavaScript 사용": "JavaScript を使う", "JavaScript 사용": "JavaScript を使う",
@ -500,6 +503,7 @@
"끝: {0}": "終了: {0}", "끝: {0}": "終了: {0}",
"끝나지 못했습니다: {0}": "完了できませんでした: {0}", "끝나지 못했습니다: {0}": "完了できませんでした: {0}",
"끝난 뒤 원본의 빈 폴더를 지웁니다 (find 원본/ -type d -empty -delete). 원본 폴더는:": "終了後、元の空フォルダーを削除します (find 元/ -type d -empty -delete)。元フォルダーは:", "끝난 뒤 원본의 빈 폴더를 지웁니다 (find 원본/ -type d -empty -delete). 원본 폴더는:": "終了後、元の空フォルダーを削除します (find 元/ -type d -empty -delete)。元フォルダーは:",
"끝난 작업": "完了した作業",
"끝내기": "終了", "끝내기": "終了",
"끝내기 (작업 멈춤)": "終了 (作業を止める)", "끝내기 (작업 멈춤)": "終了 (作業を止める)",
"끝에서 처음으로 돌기": "最後から最初に戻る", "끝에서 처음으로 돌기": "最後から最初に戻る",
@ -596,6 +600,7 @@
"다운로드 {0}": "ダウンロード {0}", "다운로드 {0}": "ダウンロード {0}",
"다운로드 {0}개": "ダウンロード {0}個", "다운로드 {0}개": "ダウンロード {0}個",
"다운로드 {0}개{1}": "ダウンロード {0}個{1}", "다운로드 {0}개{1}": "ダウンロード {0}個{1}",
"다운로드 · MKV 만들기 · AI 자막 · 동기화 진행 상황": "ダウンロード · MKV 作成 · AI 字幕 · 同期の進行状況",
"다운로드 · 변환도 모두 종료 (묻지 않음)": "ダウンロード・変換もすべて終了(確認しない)", "다운로드 · 변환도 모두 종료 (묻지 않음)": "ダウンロード・変換もすべて終了(確認しない)",
"다운로드 목록": "ダウンロードリスト", "다운로드 목록": "ダウンロードリスト",
"다운로드 목록 (지금 여기)": "ダウンロードリスト(現在地)", "다운로드 목록 (지금 여기)": "ダウンロードリスト(現在地)",
@ -1137,6 +1142,8 @@
"앱에 들어 있는 것 (기본)": "アプリに内蔵 (既定)", "앱에 들어 있는 것 (기본)": "アプリに内蔵 (既定)",
"앱을 다시 설치하자 Android 자동 백업 (Google 백업) 이 이전 설정을 되살렸습니다.\n· 동영상 목록 {0}개\n· WebDAV 서버: {1}": "アプリを再インストールしたため、Android 自動バックアップ (Google バックアップ) が以前の設定を復元しました。\n・動画リスト {0} 件\n・WebDAV サーバー: {1}", "앱을 다시 설치하자 Android 자동 백업 (Google 백업) 이 이전 설정을 되살렸습니다.\n· 동영상 목록 {0}개\n· WebDAV 서버: {1}": "アプリを再インストールしたため、Android 自動バックアップ (Google バックアップ) が以前の設定を復元しました。\n・動画リスト {0} 件\n・WebDAV サーバー: {1}",
"앱을 다시 켤 때 동기화": "アプリ再起動時の同期", "앱을 다시 켤 때 동기화": "アプリ再起動時の同期",
"앱을 닫아 진행 중이던 작업 (MKV · 다운로드 · 동기화) 이 멈췄습니다. 앱을 닫아도 계속하려면 환경 설정 > Rsync > '백그라운드로 실행' 을 켜세요.": "アプリを閉じたため実行中の作業 (MKV · ダウンロード · 同期) が停止しました。アプリを閉じても続けるには、設定 > Rsync >「バックグラウンドで実行」をオンにしてください。",
"앱을 닫아 진행 중이던 작업이 멈췄습니다. 닫아도 계속하려면 환경 설정에서 '백그라운드로 실행' 을 켜세요.": "アプリを閉じたため実行中の作業が停止しました。閉じても続けるには、設定で「バックグラウンドで実行」をオンにしてください。",
"앱을 새로 설치해 설정이 없습니다. 저장해 둔 v{0} 의 설정 (동영상 목록 · 즐겨찾기 포함) 을 되살릴까요?\n(자막 API 키 · 비밀번호는 저장하지 않았으니 다시 넣어 주세요)": "アプリを新しくインストールしたため設定がありません。保存した v{0} の設定 (動画リスト · ブックマークを含む) を復元しますか?\n(字幕 API キー · パスワードは保存していないので再入力してください)", "앱을 새로 설치해 설정이 없습니다. 저장해 둔 v{0} 의 설정 (동영상 목록 · 즐겨찾기 포함) 을 되살릴까요?\n(자막 API 키 · 비밀번호는 저장하지 않았으니 다시 넣어 주세요)": "アプリを新しくインストールしたため設定がありません。保存した v{0} の設定 (動画リスト · ブックマークを含む) を復元しますか?\n(字幕 API キー · パスワードは保存していないので再入力してください)",
"앱을 지우면 다음은 되살릴 수 없습니다:": "アプリを削除すると次のものは復元できません:", "앱을 지우면 다음은 되살릴 수 없습니다:": "アプリを削除すると次のものは復元できません:",
"앱이 켜져 있는 동안 늘 동기화합니다.": "アプリの起動中は常に同期します。", "앱이 켜져 있는 동안 늘 동기화합니다.": "アプリの起動中は常に同期します。",
@ -1471,6 +1478,7 @@
"작업 임시 파일": "作業の一時ファイル", "작업 임시 파일": "作業の一時ファイル",
"작업 중": "作業中", "작업 중": "作業中",
"작업 중지: {0}{1}": "作業中止: {0}{1}", "작업 중지: {0}{1}": "作業中止: {0}{1}",
"작업 진행": "作業の進行",
"작업 취소": "作業をキャンセル", "작업 취소": "作業をキャンセル",
"작업 현황": "作業状況", "작업 현황": "作業状況",
"작업 현황 닫기": "作業状況を閉じる", "작업 현황 닫기": "作業状況を閉じる",

View File

@ -97,6 +97,7 @@
"AI 해상도 올리기": "AI 提高分辨率", "AI 해상도 올리기": "AI 提高分辨率",
"AI 해상도 올리기 (모든 장)": "AI 提高分辨率 (全部页面)", "AI 해상도 올리기 (모든 장)": "AI 提高分辨率 (全部页面)",
"AI 해상도 올리기 (이 장)": "AI 提高分辨率 (本页)", "AI 해상도 올리기 (이 장)": "AI 提高分辨率 (本页)",
"AI 해상도 올리기 등 오래 걸린 작업이 끝났을 때": "AI 提升分辨率等耗时任务完成时",
"API 키": "API 密钥", "API 키": "API 密钥",
"API 키 (선택)": "API 密钥 (可选)", "API 키 (선택)": "API 密钥 (可选)",
"API 키 (필수, 무료)": "API 密钥(必填,免费)", "API 키 (필수, 무료)": "API 密钥(必填,免费)",
@ -134,6 +135,8 @@
"JJ_MKVMaker - 다운로드 {0}개": "JJ_MKVMaker - 下载 {0} 个", "JJ_MKVMaker - 다운로드 {0}개": "JJ_MKVMaker - 下载 {0} 个",
"JJ_MKVMaker 로 자막 만들기": "用 JJ_MKVMaker 生成字幕", "JJ_MKVMaker 로 자막 만들기": "用 JJ_MKVMaker 生成字幕",
"JJ_MKVMaker 로 재생": "用 JJ_MKVMaker 播放", "JJ_MKVMaker 로 재생": "用 JJ_MKVMaker 播放",
"JJ_MKVMaker 작업 중": "JJ_MKVMaker 正在处理",
"JJ_MKVMaker 작업이 멈췄습니다": "JJ_MKVMaker 的任务已停止",
"JJ_MKVMaker_즐겨찾기.html": "JJ_MKVMaker_书签.html", "JJ_MKVMaker_즐겨찾기.html": "JJ_MKVMaker_书签.html",
"JPG 품질 {0}": "JPG 质量 {0}", "JPG 품질 {0}": "JPG 质量 {0}",
"JavaScript 사용": "启用 JavaScript", "JavaScript 사용": "启用 JavaScript",
@ -500,6 +503,7 @@
"끝: {0}": "结束: {0}", "끝: {0}": "结束: {0}",
"끝나지 못했습니다: {0}": "未能完成: {0}", "끝나지 못했습니다: {0}": "未能完成: {0}",
"끝난 뒤 원본의 빈 폴더를 지웁니다 (find 원본/ -type d -empty -delete). 원본 폴더는:": "完成后删除源中的空文件夹 (find 源/ -type d -empty -delete)。源文件夹:", "끝난 뒤 원본의 빈 폴더를 지웁니다 (find 원본/ -type d -empty -delete). 원본 폴더는:": "完成后删除源中的空文件夹 (find 源/ -type d -empty -delete)。源文件夹:",
"끝난 작업": "已完成的任务",
"끝내기": "退出", "끝내기": "退出",
"끝내기 (작업 멈춤)": "退出 (停止任务)", "끝내기 (작업 멈춤)": "退出 (停止任务)",
"끝에서 처음으로 돌기": "到末尾后回到开头", "끝에서 처음으로 돌기": "到末尾后回到开头",
@ -596,6 +600,7 @@
"다운로드 {0}": "下载 {0}", "다운로드 {0}": "下载 {0}",
"다운로드 {0}개": "下载 {0} 个", "다운로드 {0}개": "下载 {0} 个",
"다운로드 {0}개{1}": "下载 {0} 个{1}", "다운로드 {0}개{1}": "下载 {0} 个{1}",
"다운로드 · MKV 만들기 · AI 자막 · 동기화 진행 상황": "下载 · 制作 MKV · AI 字幕 · 同步的进度",
"다운로드 · 변환도 모두 종료 (묻지 않음)": "同时退出下载 · 转换(不再询问)", "다운로드 · 변환도 모두 종료 (묻지 않음)": "同时退出下载 · 转换(不再询问)",
"다운로드 목록": "下载列表", "다운로드 목록": "下载列表",
"다운로드 목록 (지금 여기)": "下载列表(当前位置)", "다운로드 목록 (지금 여기)": "下载列表(当前位置)",
@ -1137,6 +1142,8 @@
"앱에 들어 있는 것 (기본)": "应用内置 (默认)", "앱에 들어 있는 것 (기본)": "应用内置 (默认)",
"앱을 다시 설치하자 Android 자동 백업 (Google 백업) 이 이전 설정을 되살렸습니다.\n· 동영상 목록 {0}개\n· WebDAV 서버: {1}": "重新安装应用后,Android 自动备份 (Google 备份) 恢复了之前的设置。\n· 视频列表 {0} 个\n· WebDAV 服务器: {1}", "앱을 다시 설치하자 Android 자동 백업 (Google 백업) 이 이전 설정을 되살렸습니다.\n· 동영상 목록 {0}개\n· WebDAV 서버: {1}": "重新安装应用后,Android 自动备份 (Google 备份) 恢复了之前的设置。\n· 视频列表 {0} 个\n· WebDAV 服务器: {1}",
"앱을 다시 켤 때 동기화": "应用重新启动时同步", "앱을 다시 켤 때 동기화": "应用重新启动时同步",
"앱을 닫아 진행 중이던 작업 (MKV · 다운로드 · 동기화) 이 멈췄습니다. 앱을 닫아도 계속하려면 환경 설정 > Rsync > '백그라운드로 실행' 을 켜세요.": "关闭应用后,正在进行的任务(MKV · 下载 · 同步)已停止。如需关闭应用后继续,请开启 设置 > Rsync >“在后台运行”。",
"앱을 닫아 진행 중이던 작업이 멈췄습니다. 닫아도 계속하려면 환경 설정에서 '백그라운드로 실행' 을 켜세요.": "关闭应用后,正在进行的任务已停止。如需关闭后继续,请在设置中开启“在后台运行”。",
"앱을 새로 설치해 설정이 없습니다. 저장해 둔 v{0} 의 설정 (동영상 목록 · 즐겨찾기 포함) 을 되살릴까요?\n(자막 API 키 · 비밀번호는 저장하지 않았으니 다시 넣어 주세요)": "应用为全新安装,没有设置。要恢复保存的 v{0} 设置 (包括视频列表 · 书签) 吗?\n(字幕 API 密钥 · 密码未保存,请重新输入)", "앱을 새로 설치해 설정이 없습니다. 저장해 둔 v{0} 의 설정 (동영상 목록 · 즐겨찾기 포함) 을 되살릴까요?\n(자막 API 키 · 비밀번호는 저장하지 않았으니 다시 넣어 주세요)": "应用为全新安装,没有设置。要恢复保存的 v{0} 设置 (包括视频列表 · 书签) 吗?\n(字幕 API 密钥 · 密码未保存,请重新输入)",
"앱을 지우면 다음은 되살릴 수 없습니다:": "删除应用后以下内容无法恢复:", "앱을 지우면 다음은 되살릴 수 없습니다:": "删除应用后以下内容无法恢复:",
"앱이 켜져 있는 동안 늘 동기화합니다.": "应用运行期间始终同步。", "앱이 켜져 있는 동안 늘 동기화합니다.": "应用运行期间始终同步。",
@ -1471,6 +1478,7 @@
"작업 임시 파일": "任务临时文件", "작업 임시 파일": "任务临时文件",
"작업 중": "处理中", "작업 중": "处理中",
"작업 중지: {0}{1}": "任务已停止: {0}{1}", "작업 중지: {0}{1}": "任务已停止: {0}{1}",
"작업 진행": "任务进度",
"작업 취소": "取消任务", "작업 취소": "取消任务",
"작업 현황": "任务状态", "작업 현황": "任务状态",
"작업 현황 닫기": "关闭任务状态", "작업 현황 닫기": "关闭任务状态",

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../../app/app_controller.dart'; import '../../app/app_controller.dart';
import '../../app/i18n_controller.dart';
import '../../app/ai_local.dart'; import '../../app/ai_local.dart';
import '../../app/download_manager.dart'; import '../../app/download_manager.dart';
import '../../app/copy_center.dart'; import '../../app/copy_center.dart';
@ -27,6 +28,7 @@ class AndroidKeepAlive {
c.addListener(_changed); c.addListener(_changed);
downloads.addListener(_changed); downloads.addListener(_changed);
AiJobs.instance.addListener(_changed); // 123: AI 그림도 화면을 닫아도 계속 AiJobs.instance.addListener(_changed); // 123: AI 그림도 화면을 닫아도 계속
i18n.addListener(_changed); // 57: 알림 글 · 채널 이름도 화면 언어로
_changed(); _changed();
} }
@ -34,6 +36,19 @@ class AndroidKeepAlive {
CopyCenter? _copies; CopyCenter? _copies;
AiStore? _ai; AiStore? _ai;
bool? _background; bool? _background;
String? _textsKey;
/// 57: 알림 제목 · "작업 멈춤" 알림 · 알림 채널 이름 (Android 설정 > 알림 에 보이는 이름) 을 화면 언어로
static Map<String, String> notificationTexts() => {
'working': tr('JJ_MKVMaker 작업 중'),
'stoppedTitle': tr('JJ_MKVMaker 작업이 멈췄습니다'),
'stoppedText': tr("앱을 닫아 진행 중이던 작업이 멈췄습니다. 닫아도 계속하려면 환경 설정에서 '백그라운드로 실행' 을 켜세요."),
'stoppedLong': tr("앱을 닫아 진행 중이던 작업 (MKV · 다운로드 · 동기화) 이 멈췄습니다. 앱을 닫아도 계속하려면 환경 설정 > Rsync > '백그라운드로 실행' 을 켜세요."),
'jobsChannel': tr('작업 진행'),
'jobsChannelDesc': tr('다운로드 · MKV 만들기 · AI 자막 · 동기화 진행 상황'),
'doneChannel': tr('끝난 작업'),
'doneChannelDesc': tr('AI 해상도 올리기 등 오래 걸린 작업이 끝났을 때'),
};
/// 진행률 알림은 자주 바뀌므로 1초에 한 번만 보낸다 /// 진행률 알림은 자주 바뀌므로 1초에 한 번만 보낸다
void _changed() => _timer ??= Timer(const Duration(seconds: 1), _apply); void _changed() => _timer ??= Timer(const Duration(seconds: 1), _apply);
@ -46,6 +61,14 @@ class AndroidKeepAlive {
_live?.removeListener(_changed); _live?.removeListener(_changed);
_live = live?..addListener(_changed); _live = live?..addListener(_changed);
} }
final texts = notificationTexts();
final textsKey = texts.values.join('\u0000');
if (textsKey != _textsKey) {
try {
await _ch.invokeMethod<void>('notificationTexts', texts);
_textsKey = textsKey;
} catch (_) {}
}
final bg = c.settings.runInBackground; final bg = c.settings.runInBackground;
if (bg != _background) { if (bg != _background) {
_background = bg; _background = bg;

View File

@ -0,0 +1,32 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/i18n_controller.dart';
import 'package:jj_mkvmaker/platform/android/android_keep_alive.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('57: Android 알림 글 · 채널 이름이 화면 언어로 (Kotlin 이 아는 열쇠와 같게)', () async {
final ko = AndroidKeepAlive.notificationTexts();
// Kotlin (KeepAliveService.defaults) 의 열쇠와 한국어 기본 글이 Dart 와 같다 - 한쪽만 고치면 어긋난다
final kt = File('android/app/src/main/kotlin/com/jj/jj_mkvmaker/KeepAliveService.kt').readAsStringSync();
final defaults = {
for (final m in RegExp(r'"(\w+)" to "((?:[^"\\]|\\.)*)"').allMatches(kt)) m.group(1)!: m.group(2)!,
};
expect(defaults, ko);
await i18n.apply('ja', save: false);
try {
final ja = AndroidKeepAlive.notificationTexts();
expect(ja['working'], 'JJ_MKVMaker 作業中');
expect(ja['jobsChannel'], '作業の進行');
expect(ja['doneChannel'], '完了した作業');
for (final k in ko.keys) {
expect(ja[k], isNot(ko[k]), reason: '$k 가 번역되지 않음');
}
} finally {
await i18n.apply('ko', save: false);
}
});
}