i18n(alarm): localize all native notification, channel and chooser texts
Build & Deploy PluriWave / Análisis de código (push) Successful in 40s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m33s

Centralize every native-side user-facing string in a single
AlarmNotificationStrings store written by Flutter via a new
setNotificationStrings MethodChannel whenever the app locale changes,
and read at notification/channel build time (with English fallbacks)
even when the engine is dead. This replaces the hardcoded Spanish text
in the ringing notification ("Alarma PluriWave", "Posponer", "Detener"),
the pre-notice notification ("Posponer", "Omitir esta vez"), both
notification channels (names + descriptions) and the file-action
choosers ("Abrir carpeta", "Abrir grabación").

The per-alarm preNoticeTemplate/snoozeCountdown template+label args are
dropped from scheduleAlarm and the persisted spec and folded into the
shared store, so a locale change now also relocalizes already-scheduled
alarms. Channels are re-created on each use so their name/description
refresh after a language switch.

Adds alarmRingingNotificationTitle, alarmFire/PreNoticeChannelName,
alarmFire/PreNoticeChannelDescription and openFolder/openRecording
chooser keys across all 13 locales (reusing snoozeAction, stopAlarmAction,
skipNextAction, snoozeAgainAction). Rewrites the template test around
setNotificationStrings. Kotlin is static-reviewed only; no Android build
environment available here.
This commit is contained in:
Javier Bautista Fernández
2026-06-30 15:41:29 +02:00
parent 481944815f
commit ffd09a2179
34 changed files with 639 additions and 144 deletions
@@ -0,0 +1,72 @@
package es.freetimelab.pluriwave
import android.content.Context
/**
* Localized strings for native alarm notifications, channels and choosers.
*
* Flutter is the single source of truth for i18n: it pushes the current-locale
* strings via the `setNotificationStrings` MethodChannel whenever the app locale
* is (re)configured. They are persisted in device-protected storage so the
* native side can read them when building a notification or channel even while
* the Flutter engine is dead (alarm fired from a killed app, after reboot, in
* direct-boot). Every getter falls back to English when a value is unset.
*/
object AlarmNotificationStrings {
private const val PREFS = "pluriwave_alarm_strings"
const val KEY_RING_TITLE = "ringTitle"
const val KEY_SNOOZE = "snoozeLabel"
const val KEY_STOP = "stopLabel"
const val KEY_SKIP = "skipLabel"
const val KEY_SNOOZE_AGAIN = "snoozeAgainLabel"
const val KEY_FIRE_CHANNEL_NAME = "fireChannelName"
const val KEY_FIRE_CHANNEL_DESC = "fireChannelDescription"
const val KEY_PRE_NOTICE_CHANNEL_NAME = "preNoticeChannelName"
const val KEY_PRE_NOTICE_CHANNEL_DESC = "preNoticeChannelDescription"
const val KEY_PRE_NOTICE_TEMPLATE = "preNoticeTemplate"
const val KEY_SNOOZE_COUNTDOWN_TEMPLATE = "snoozeCountdownTemplate"
const val KEY_OPEN_FOLDER = "openFolderTitle"
const val KEY_OPEN_RECORDING = "openRecordingTitle"
/** Persists the localized strings pushed by Flutter. Blank values are removed. */
fun save(context: Context, values: Map<String, Any?>) {
val editor = prefs(context).edit()
for ((key, value) in values) {
val str = value as? String
if (str.isNullOrBlank()) editor.remove(key) else editor.putString(key, str)
}
editor.apply()
}
fun ringTitle(context: Context) = get(context, KEY_RING_TITLE, "PluriWave alarm")
fun snoozeLabel(context: Context) = get(context, KEY_SNOOZE, "Snooze")
fun stopLabel(context: Context) = get(context, KEY_STOP, "Stop")
fun skipLabel(context: Context) = get(context, KEY_SKIP, "Skip this time")
fun snoozeAgainLabel(context: Context) = get(context, KEY_SNOOZE_AGAIN, "Snooze again")
fun fireChannelName(context: Context) = get(context, KEY_FIRE_CHANNEL_NAME, "Ringing alarms")
fun fireChannelDescription(context: Context) =
get(context, KEY_FIRE_CHANNEL_DESC, "Urgent sound and screen when a music alarm must ring")
fun preNoticeChannelName(context: Context) =
get(context, KEY_PRE_NOTICE_CHANNEL_NAME, "Alarm reminders")
fun preNoticeChannelDescription(context: Context) =
get(context, KEY_PRE_NOTICE_CHANNEL_DESC, "Silent notifications before the alarm")
fun openFolderTitle(context: Context) = get(context, KEY_OPEN_FOLDER, "Open folder")
fun openRecordingTitle(context: Context) = get(context, KEY_OPEN_RECORDING, "Open recording")
fun preNoticeText(context: Context, minutes: Long): String =
format(get(context, KEY_PRE_NOTICE_TEMPLATE, "Starts in {minutes} min"), minutes)
fun snoozeCountdownText(context: Context, minutes: Long): String =
format(get(context, KEY_SNOOZE_COUNTDOWN_TEMPLATE, "Rings in {minutes} min"), minutes)
private fun format(template: String, minutes: Long): String =
template.replace("{minutes}", minutes.toString())
private fun get(context: Context, key: String, fallback: String): String =
prefs(context).getString(key, null)?.takeIf { it.isNotBlank() } ?: fallback
private fun prefs(context: Context) =
context.applicationContext.createDeviceProtectedStorageContext()
.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
}
@@ -42,11 +42,7 @@ class AlarmScheduler(private val context: Context) {
snoozeMinutes: Int = 5, snoozeMinutes: Int = 5,
fallbackStationName: String? = null, fallbackStationName: String? = null,
fallbackStationUrl: String? = null, fallbackStationUrl: String? = null,
fadeInSegundos: Int = 0, fadeInSegundos: Int = 0
preNoticeTemplate: String? = null,
snoozeCountdownTemplate: String? = null,
snoozeAgainLabel: String? = null,
snoozeStopLabel: String? = null
): Boolean { ): Boolean {
val existing = readSpec(id) val existing = readSpec(id)
val preservedSnooze = preserveNativeSnooze( val preservedSnooze = preserveNativeSnooze(
@@ -77,11 +73,7 @@ class AlarmScheduler(private val context: Context) {
fallbackSound = fallbackSound, fallbackSound = fallbackSound,
volume = volume.coerceIn(0f, 1f), volume = volume.coerceIn(0f, 1f),
fadeInSegundos = fadeInSegundos.coerceIn(0, 60), fadeInSegundos = fadeInSegundos.coerceIn(0, 60),
timezoneId = TimeZone.getDefault().id, timezoneId = TimeZone.getDefault().id
preNoticeTemplate = preNoticeTemplate,
snoozeCountdownTemplate = snoozeCountdownTemplate,
snoozeAgainLabel = snoozeAgainLabel,
snoozeStopLabel = snoozeStopLabel
) )
return scheduleSpec(spec, persistOnSuccess = true) return scheduleSpec(spec, persistOnSuccess = true)
} }
@@ -168,7 +160,6 @@ class AlarmScheduler(private val context: Context) {
PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT, PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT,
spec.snoozeOriginMillis ?: spec.triggerAtMillis spec.snoozeOriginMillis ?: spec.triggerAtMillis
) )
putExtra(EXTRA_PRE_NOTICE_TEMPLATE, spec.preNoticeTemplate)
}, },
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
) )
@@ -189,7 +180,6 @@ class AlarmScheduler(private val context: Context) {
PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT, PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT,
spec.snoozeOriginMillis ?: spec.triggerAtMillis spec.snoozeOriginMillis ?: spec.triggerAtMillis
) )
putExtra(EXTRA_PRE_NOTICE_TEMPLATE, spec.preNoticeTemplate)
} }
) )
Log.d(tag, "alarm.schedule preNotice immediate id=${spec.id}") Log.d(tag, "alarm.schedule preNotice immediate id=${spec.id}")
@@ -483,11 +473,7 @@ class AlarmScheduler(private val context: Context) {
private fun postSnoozeCountdownNotification(spec: NativeAlarmSpec, remaining: Long) { private fun postSnoozeCountdownNotification(spec: NativeAlarmSpec, remaining: Long) {
ensurePreNoticeChannel() ensurePreNoticeChannel()
val text = if (spec.snoozeCountdownTemplate.isNullOrBlank()) { val text = AlarmNotificationStrings.snoozeCountdownText(appContext, remaining)
"Rings in $remaining min"
} else {
spec.snoozeCountdownTemplate.replace("{minutes}", remaining.toString())
}
val openIntent = PendingIntent.getActivity( val openIntent = PendingIntent.getActivity(
appContext, appContext,
@@ -531,8 +517,8 @@ class AlarmScheduler(private val context: Context) {
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
.setOngoing(true) .setOngoing(true)
.setContentIntent(openIntent) .setContentIntent(openIntent)
.addAction(0, spec.snoozeAgainLabel ?: "Snooze again", againIntent) .addAction(0, AlarmNotificationStrings.snoozeAgainLabel(appContext), againIntent)
.addAction(0, spec.snoozeStopLabel ?: "Stop", stopIntent) .addAction(0, AlarmNotificationStrings.stopLabel(appContext), stopIntent)
.build() .build()
try { try {
@@ -548,12 +534,14 @@ class AlarmScheduler(private val context: Context) {
private fun ensurePreNoticeChannel() { private fun ensurePreNoticeChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val manager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val manager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (manager.getNotificationChannel(PluriWaveAlarmReceiver.CHANNEL_ID) != null) return // Re-create each time so the localized name/description refresh after a
// locale change (Android updates them on an existing channel).
val channel = NotificationChannel( val channel = NotificationChannel(
PluriWaveAlarmReceiver.CHANNEL_ID, PluriWaveAlarmReceiver.CHANNEL_ID,
"Preavisos de alarmas", AlarmNotificationStrings.preNoticeChannelName(appContext),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
).apply { ).apply {
description = AlarmNotificationStrings.preNoticeChannelDescription(appContext)
setSound(null, null) setSound(null, null)
enableVibration(false) enableVibration(false)
} }
@@ -905,15 +893,7 @@ class AlarmScheduler(private val context: Context) {
val fallbackSound: String?, val fallbackSound: String?,
val volume: Float, val volume: Float,
val fadeInSegundos: Int = 0, val fadeInSegundos: Int = 0,
val timezoneId: String, val timezoneId: String
// Nullable for backward compat: old persisted alarms without this field
// fall back to the English default in the receiver. Schema stays v3.
val preNoticeTemplate: String? = null,
// Localized template + button labels for the per-minute snooze countdown
// notification. Nullable so pre-existing persisted alarms keep working.
val snoozeCountdownTemplate: String? = null,
val snoozeAgainLabel: String? = null,
val snoozeStopLabel: String? = null
) { ) {
fun toJson(): JSONObject = JSONObject().apply { fun toJson(): JSONObject = JSONObject().apply {
put("schemaVersion", 3) put("schemaVersion", 3)
@@ -940,10 +920,6 @@ class AlarmScheduler(private val context: Context) {
put("volume", volume) put("volume", volume)
put("fadeInSegundos", fadeInSegundos) put("fadeInSegundos", fadeInSegundos)
put("timezoneId", timezoneId) put("timezoneId", timezoneId)
put("preNoticeTemplate", preNoticeTemplate)
put("snoozeCountdownTemplate", snoozeCountdownTemplate)
put("snoozeAgainLabel", snoozeAgainLabel)
put("snoozeStopLabel", snoozeStopLabel)
} }
companion object { companion object {
@@ -980,12 +956,7 @@ class AlarmScheduler(private val context: Context) {
fallbackSound = json.optString("fallbackSound").takeIf { it.isNotBlank() }, fallbackSound = json.optString("fallbackSound").takeIf { it.isNotBlank() },
volume = json.optDouble("volume", 0.85).toFloat(), volume = json.optDouble("volume", 0.85).toFloat(),
fadeInSegundos = json.optInt("fadeInSegundos", 0).coerceIn(0, 60), fadeInSegundos = json.optInt("fadeInSegundos", 0).coerceIn(0, 60),
timezoneId = json.optString("timezoneId", TimeZone.getDefault().id), timezoneId = json.optString("timezoneId", TimeZone.getDefault().id)
preNoticeTemplate = json.optString("preNoticeTemplate").takeIf { it.isNotBlank() },
snoozeCountdownTemplate =
json.optString("snoozeCountdownTemplate").takeIf { it.isNotBlank() },
snoozeAgainLabel = json.optString("snoozeAgainLabel").takeIf { it.isNotBlank() },
snoozeStopLabel = json.optString("snoozeStopLabel").takeIf { it.isNotBlank() }
) )
} }
} }
@@ -1000,9 +971,6 @@ class AlarmScheduler(private val context: Context) {
private const val PRE_NOTICE_MILLIS = 30 * 60 * 1000L private const val PRE_NOTICE_MILLIS = 30 * 60 * 1000L
private const val SCHEDULE_UNICA = "unica" private const val SCHEDULE_UNICA = "unica"
private const val SCHEDULE_DIAS_SEMANA = "diasSemana" private const val SCHEDULE_DIAS_SEMANA = "diasSemana"
// Intent extra key for the localized pre-notice template string.
// Declared once here; PluriWaveAlarmReceiver reads it via this constant.
const val EXTRA_PRE_NOTICE_TEMPLATE = "preNoticeTemplate"
} }
} }
@@ -115,11 +115,7 @@ class MainActivity : AudioServiceActivity() {
snoozeMinutes = call.argument<Int>("snoozeMinutes") ?: 5, snoozeMinutes = call.argument<Int>("snoozeMinutes") ?: 5,
fallbackStationName = call.argument<String>("fallbackStationName"), fallbackStationName = call.argument<String>("fallbackStationName"),
fallbackStationUrl = call.argument<String>("fallbackStationUrl"), fallbackStationUrl = call.argument<String>("fallbackStationUrl"),
fadeInSegundos = call.argument<Int>("fadeInSegundos") ?: 0, fadeInSegundos = call.argument<Int>("fadeInSegundos") ?: 0
preNoticeTemplate = call.argument<String>("preNoticeTemplate"),
snoozeCountdownTemplate = call.argument<String>("snoozeCountdownTemplate"),
snoozeAgainLabel = call.argument<String>("snoozeAgainLabel"),
snoozeStopLabel = call.argument<String>("snoozeStopLabel")
) )
result.success(scheduled) result.success(scheduled)
} }
@@ -209,6 +205,16 @@ class MainActivity : AudioServiceActivity() {
Log.d(tag, "alarm.channel getNativeSnoozeState") Log.d(tag, "alarm.channel getNativeSnoozeState")
result.success(alarmScheduler.nativeSnoozeStates()) result.success(alarmScheduler.nativeSnoozeStates())
} }
"setNotificationStrings" -> {
val args = call.arguments as? Map<*, *>
if (args != null) {
AlarmNotificationStrings.save(
this,
args.entries.associate { (k, v) -> k.toString() to v }
)
}
result.success(null)
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@@ -427,7 +433,9 @@ class MainActivity : AudioServiceActivity() {
for (intent in candidates) { for (intent in candidates) {
try { try {
startActivity(Intent.createChooser(intent, "Abrir carpeta")) startActivity(
Intent.createChooser(intent, AlarmNotificationStrings.openFolderTitle(this))
)
Log.d(tag, "file_actions.viewDirectory launched path=$path") Log.d(tag, "file_actions.viewDirectory launched path=$path")
return true return true
} catch (_: ActivityNotFoundException) { } catch (_: ActivityNotFoundException) {
@@ -471,7 +479,9 @@ class MainActivity : AudioServiceActivity() {
clipData = ClipData.newUri(contentResolver, "recording", uri) clipData = ClipData.newUri(contentResolver, "recording", uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
startActivity(Intent.createChooser(intent, "Abrir grabación")) startActivity(
Intent.createChooser(intent, AlarmNotificationStrings.openRecordingTitle(this))
)
Log.d(tag, "file_actions.openFile launched path=$path") Log.d(tag, "file_actions.openFile launched path=$path")
true true
} catch (_: ActivityNotFoundException) { } catch (_: ActivityNotFoundException) {
@@ -51,8 +51,7 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
title, title,
snoozeMinutes, snoozeMinutes,
intent.getLongExtra(EXTRA_TRIGGER_AT, 0L), intent.getLongExtra(EXTRA_TRIGGER_AT, 0L),
intent.getLongExtra(EXTRA_OCCURRENCE_AT, 0L), intent.getLongExtra(EXTRA_OCCURRENCE_AT, 0L)
intent.getStringExtra(AlarmScheduler.EXTRA_PRE_NOTICE_TEMPLATE)
) )
} }
ACTION_POSTPONE_NEXT -> { ACTION_POSTPONE_NEXT -> {
@@ -135,13 +134,12 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
title: String, title: String,
snoozeMinutes: Int, snoozeMinutes: Int,
triggerAtMillis: Long, triggerAtMillis: Long,
occurrenceAtMillis: Long, occurrenceAtMillis: Long
preNoticeTemplate: String? = null
) { ) {
ensureChannel(context) ensureChannel(context)
val remaining = computeRemainingMinutes(triggerAtMillis) val remaining = computeRemainingMinutes(triggerAtMillis)
val contentText = formatPreNoticeText(preNoticeTemplate, remaining) val contentText = AlarmNotificationStrings.preNoticeText(context, remaining)
val openAppIntent = PendingIntent.getActivity( val openAppIntent = PendingIntent.getActivity(
context, context,
@@ -189,8 +187,8 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
.setSilent(true) .setSilent(true)
.setAutoCancel(true) .setAutoCancel(true)
.setContentIntent(openAppIntent) .setContentIntent(openAppIntent)
.addAction(0, "Posponer", postponeNextIntent) .addAction(0, AlarmNotificationStrings.snoozeLabel(context), postponeNextIntent)
.addAction(0, "Omitir esta vez", skipNextIntent) .addAction(0, AlarmNotificationStrings.skipLabel(context), skipNextIntent)
.build() .build()
try { try {
@@ -208,30 +206,17 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
private fun computeRemainingMinutes(triggerAtMillis: Long): Long = private fun computeRemainingMinutes(triggerAtMillis: Long): Long =
maxOf(1L, (triggerAtMillis - System.currentTimeMillis()) / 60_000L) maxOf(1L, (triggerAtMillis - System.currentTimeMillis()) / 60_000L)
/**
* Formats the pre-notice notification text by replacing the `{minutes}`
* placeholder in [template] with [remaining]. Falls back to an English
* default if [template] is null or blank.
*/
private fun formatPreNoticeText(template: String?, remaining: Long): String {
if (template.isNullOrBlank()) {
return "Starts in $remaining min"
}
return template.replace("{minutes}", remaining.toString())
}
private fun ensureChannel(context: Context) { private fun ensureChannel(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val existing = manager.getNotificationChannel(CHANNEL_ID) // Re-create each time so the localized name/description refresh after a
if (existing != null) return // locale change (Android updates them on an existing channel).
val channel = NotificationChannel( val channel = NotificationChannel(
CHANNEL_ID, CHANNEL_ID,
"Preavisos de alarmas", AlarmNotificationStrings.preNoticeChannelName(context),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
).apply { ).apply {
description = "Notificaciones silenciosas 30 minutos antes de la alarma" description = AlarmNotificationStrings.preNoticeChannelDescription(context)
setSound(null, null) setSound(null, null)
enableVibration(false) enableVibration(false)
} }
@@ -388,7 +388,7 @@ class PluriWaveAlarmService : Service() {
) = ) =
NotificationCompat.Builder(this, CHANNEL_ID) NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_lock_idle_alarm) .setSmallIcon(android.R.drawable.ic_lock_idle_alarm)
.setContentTitle("Alarma PluriWave") .setContentTitle(AlarmNotificationStrings.ringTitle(this))
.setContentText( .setContentText(
if (stationName.isNullOrBlank()) title else "$title - $stationName" if (stationName.isNullOrBlank()) title else "$title - $stationName"
) )
@@ -399,8 +399,8 @@ class PluriWaveAlarmService : Service() {
.setAutoCancel(false) .setAutoCancel(false)
.setFullScreenIntent(openAlarmPendingIntent(alarmId, title, snoozeMinutes), true) .setFullScreenIntent(openAlarmPendingIntent(alarmId, title, snoozeMinutes), true)
.setContentIntent(openAlarmPendingIntent(alarmId, title, snoozeMinutes)) .setContentIntent(openAlarmPendingIntent(alarmId, title, snoozeMinutes))
.addAction(0, "Posponer", snoozePendingIntent(alarmId, snoozeMinutes)) .addAction(0, AlarmNotificationStrings.snoozeLabel(this), snoozePendingIntent(alarmId, snoozeMinutes))
.addAction(0, "Detener", stopPendingIntent(alarmId)) .addAction(0, AlarmNotificationStrings.stopLabel(this), stopPendingIntent(alarmId))
.build() .build()
private fun openAlarmPendingIntent( private fun openAlarmPendingIntent(
@@ -553,13 +553,16 @@ class PluriWaveAlarmService : Service() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
migrateLegacyChannels(context, manager) migrateLegacyChannels(context, manager)
if (manager.getNotificationChannel(CHANNEL_ID) != null) return // Re-create each time (not early-returning when present) so the
// localized name/description refresh after a locale change. Android
// updates name + description on an existing channel; importance and
// sound stay fixed from first creation.
val channel = NotificationChannel( val channel = NotificationChannel(
CHANNEL_ID, CHANNEL_ID,
"Alarmas sonando", AlarmNotificationStrings.fireChannelName(context),
NotificationManager.IMPORTANCE_HIGH NotificationManager.IMPORTANCE_HIGH
).apply { ).apply {
description = "Sonido y pantalla urgente cuando una alarma musical debe sonar" description = AlarmNotificationStrings.fireChannelDescription(context)
enableVibration(true) enableVibration(true)
setSound( setSound(
Settings.System.DEFAULT_ALARM_ALERT_URI, Settings.System.DEFAULT_ALARM_ALERT_URI,
+7
View File
@@ -638,6 +638,13 @@
} }
}, },
"snoozeAgainAction": "غفوة مرة أخرى", "snoozeAgainAction": "غفوة مرة أخرى",
"alarmRingingNotificationTitle": "منبّه PluriWave",
"alarmFireChannelName": "المنبّهات الرنّانة",
"alarmFireChannelDescription": "صوت وشاشة عاجلة عندما يجب أن يرنّ منبّه موسيقي",
"alarmPreNoticeChannelName": "تنبيهات المنبّهات",
"alarmPreNoticeChannelDescription": "إشعارات صامتة قبل المنبّه",
"openFolderChooserTitle": "فتح المجلد",
"openRecordingChooserTitle": "فتح التسجيل",
"@preNoticeCountdown": { "@preNoticeCountdown": {
"placeholders": { "placeholders": {
"minutes": { "minutes": {
+7
View File
@@ -638,6 +638,13 @@
} }
}, },
"snoozeAgainAction": "আবার স্নুজ করুন", "snoozeAgainAction": "আবার স্নুজ করুন",
"alarmRingingNotificationTitle": "PluriWave অ্যালার্ম",
"alarmFireChannelName": "বাজতে থাকা অ্যালার্ম",
"alarmFireChannelDescription": "কোনো মিউজিক অ্যালার্ম বাজার সময় জরুরি শব্দ ও স্ক্রিন",
"alarmPreNoticeChannelName": "অ্যালার্ম রিমাইন্ডার",
"alarmPreNoticeChannelDescription": "অ্যালার্মের আগে নীরব বিজ্ঞপ্তি",
"openFolderChooserTitle": "ফোল্ডার খুলুন",
"openRecordingChooserTitle": "রেকর্ডিং খুলুন",
"@preNoticeCountdown": { "@preNoticeCountdown": {
"placeholders": { "placeholders": {
"minutes": { "minutes": {
+7
View File
@@ -638,6 +638,13 @@
} }
}, },
"snoozeAgainAction": "Erneut schlummern", "snoozeAgainAction": "Erneut schlummern",
"alarmRingingNotificationTitle": "PluriWave-Wecker",
"alarmFireChannelName": "Klingelnde Wecker",
"alarmFireChannelDescription": "Dringender Ton und Bildschirm, wenn ein Musikwecker klingeln soll",
"alarmPreNoticeChannelName": "Wecker-Vorankündigungen",
"alarmPreNoticeChannelDescription": "Lautlose Benachrichtigungen vor dem Wecker",
"openFolderChooserTitle": "Ordner öffnen",
"openRecordingChooserTitle": "Aufnahme öffnen",
"@preNoticeCountdown": { "@preNoticeCountdown": {
"placeholders": { "placeholders": {
"minutes": { "minutes": {
+7
View File
@@ -638,6 +638,13 @@
} }
}, },
"snoozeAgainAction": "Snooze again", "snoozeAgainAction": "Snooze again",
"alarmRingingNotificationTitle": "PluriWave alarm",
"alarmFireChannelName": "Ringing alarms",
"alarmFireChannelDescription": "Urgent sound and screen when a music alarm must ring",
"alarmPreNoticeChannelName": "Alarm reminders",
"alarmPreNoticeChannelDescription": "Silent notifications before the alarm",
"openFolderChooserTitle": "Open folder",
"openRecordingChooserTitle": "Open recording",
"@preNoticeCountdown": { "@preNoticeCountdown": {
"placeholders": { "placeholders": {
"minutes": { "minutes": {
+7
View File
@@ -601,6 +601,13 @@
} }
}, },
"snoozeAgainAction": "Posponer otra vez", "snoozeAgainAction": "Posponer otra vez",
"alarmRingingNotificationTitle": "Alarma PluriWave",
"alarmFireChannelName": "Alarmas sonando",
"alarmFireChannelDescription": "Sonido y pantalla urgente cuando una alarma musical debe sonar",
"alarmPreNoticeChannelName": "Preavisos de alarmas",
"alarmPreNoticeChannelDescription": "Notificaciones silenciosas antes de la alarma",
"openFolderChooserTitle": "Abrir carpeta",
"openRecordingChooserTitle": "Abrir grabación",
"@preNoticeCountdown": { "@preNoticeCountdown": {
"placeholders": { "placeholders": {
"minutes": { "minutes": {
+7
View File
@@ -638,6 +638,13 @@
} }
}, },
"snoozeAgainAction": "Reporter encore", "snoozeAgainAction": "Reporter encore",
"alarmRingingNotificationTitle": "Alarme PluriWave",
"alarmFireChannelName": "Alarmes en cours",
"alarmFireChannelDescription": "Son et écran urgents lorsqu'une alarme musicale doit sonner",
"alarmPreNoticeChannelName": "Rappels d'alarme",
"alarmPreNoticeChannelDescription": "Notifications silencieuses avant l'alarme",
"openFolderChooserTitle": "Ouvrir le dossier",
"openRecordingChooserTitle": "Ouvrir l'enregistrement",
"@preNoticeCountdown": { "@preNoticeCountdown": {
"placeholders": { "placeholders": {
"minutes": { "minutes": {
+7
View File
@@ -638,6 +638,13 @@
} }
}, },
"snoozeAgainAction": "फिर से स्नूज़ करें", "snoozeAgainAction": "फिर से स्नूज़ करें",
"alarmRingingNotificationTitle": "PluriWave अलार्म",
"alarmFireChannelName": "बजते अलार्म",
"alarmFireChannelDescription": "जब कोई संगीत अलार्म बजना हो तो तत्काल ध्वनि और स्क्रीन",
"alarmPreNoticeChannelName": "अलार्म रिमाइंडर",
"alarmPreNoticeChannelDescription": "अलार्म से पहले मूक सूचनाएँ",
"openFolderChooserTitle": "फ़ोल्डर खोलें",
"openRecordingChooserTitle": "रिकॉर्डिंग खोलें",
"@preNoticeCountdown": { "@preNoticeCountdown": {
"placeholders": { "placeholders": {
"minutes": { "minutes": {
+7
View File
@@ -638,6 +638,13 @@
} }
}, },
"snoozeAgainAction": "Tunda lagi", "snoozeAgainAction": "Tunda lagi",
"alarmRingingNotificationTitle": "Alarm PluriWave",
"alarmFireChannelName": "Alarm berbunyi",
"alarmFireChannelDescription": "Suara dan layar mendesak saat alarm musik harus berbunyi",
"alarmPreNoticeChannelName": "Pengingat alarm",
"alarmPreNoticeChannelDescription": "Notifikasi senyap sebelum alarm",
"openFolderChooserTitle": "Buka folder",
"openRecordingChooserTitle": "Buka rekaman",
"@preNoticeCountdown": { "@preNoticeCountdown": {
"placeholders": { "placeholders": {
"minutes": { "minutes": {
+7
View File
@@ -638,6 +638,13 @@
} }
}, },
"snoozeAgainAction": "Posponi di nuovo", "snoozeAgainAction": "Posponi di nuovo",
"alarmRingingNotificationTitle": "Sveglia PluriWave",
"alarmFireChannelName": "Sveglie in corso",
"alarmFireChannelDescription": "Suono e schermo urgenti quando una sveglia musicale deve suonare",
"alarmPreNoticeChannelName": "Promemoria sveglia",
"alarmPreNoticeChannelDescription": "Notifiche silenziose prima della sveglia",
"openFolderChooserTitle": "Apri cartella",
"openRecordingChooserTitle": "Apri registrazione",
"@preNoticeCountdown": { "@preNoticeCountdown": {
"placeholders": { "placeholders": {
"minutes": { "minutes": {
+7
View File
@@ -638,6 +638,13 @@
} }
}, },
"snoozeAgainAction": "もう一度スヌーズ", "snoozeAgainAction": "もう一度スヌーズ",
"alarmRingingNotificationTitle": "PluriWave アラーム",
"alarmFireChannelName": "鳴っているアラーム",
"alarmFireChannelDescription": "音楽アラームが鳴るときの緊急の音と画面",
"alarmPreNoticeChannelName": "アラームの事前通知",
"alarmPreNoticeChannelDescription": "アラーム前のサイレント通知",
"openFolderChooserTitle": "フォルダを開く",
"openRecordingChooserTitle": "録音を開く",
"@preNoticeCountdown": { "@preNoticeCountdown": {
"placeholders": { "placeholders": {
"minutes": { "minutes": {
+7
View File
@@ -638,6 +638,13 @@
} }
}, },
"snoozeAgainAction": "Adiar novamente", "snoozeAgainAction": "Adiar novamente",
"alarmRingingNotificationTitle": "Alarme PluriWave",
"alarmFireChannelName": "Alarmes tocando",
"alarmFireChannelDescription": "Som e tela urgentes quando um alarme musical deve tocar",
"alarmPreNoticeChannelName": "Avisos de alarme",
"alarmPreNoticeChannelDescription": "Notificações silenciosas antes do alarme",
"openFolderChooserTitle": "Abrir pasta",
"openRecordingChooserTitle": "Abrir gravação",
"@preNoticeCountdown": { "@preNoticeCountdown": {
"placeholders": { "placeholders": {
"minutes": { "minutes": {
+7
View File
@@ -638,6 +638,13 @@
} }
}, },
"snoozeAgainAction": "Отложить снова", "snoozeAgainAction": "Отложить снова",
"alarmRingingNotificationTitle": "Будильник PluriWave",
"alarmFireChannelName": "Звонящие будильники",
"alarmFireChannelDescription": "Срочный звук и экран, когда должен сработать музыкальный будильник",
"alarmPreNoticeChannelName": "Напоминания о будильнике",
"alarmPreNoticeChannelDescription": "Беззвучные уведомления перед будильником",
"openFolderChooserTitle": "Открыть папку",
"openRecordingChooserTitle": "Открыть запись",
"@preNoticeCountdown": { "@preNoticeCountdown": {
"placeholders": { "placeholders": {
"minutes": { "minutes": {
+7
View File
@@ -638,6 +638,13 @@
} }
}, },
"snoozeAgainAction": "再次小睡", "snoozeAgainAction": "再次小睡",
"alarmRingingNotificationTitle": "PluriWave 闹钟",
"alarmFireChannelName": "响铃的闹钟",
"alarmFireChannelDescription": "音乐闹钟需要响起时的紧急声音和屏幕",
"alarmPreNoticeChannelName": "闹钟预告",
"alarmPreNoticeChannelDescription": "闹钟前的静音通知",
"openFolderChooserTitle": "打开文件夹",
"openRecordingChooserTitle": "打开录音",
"@preNoticeCountdown": { "@preNoticeCountdown": {
"placeholders": { "placeholders": {
"minutes": { "minutes": {
+42
View File
@@ -2324,6 +2324,48 @@ abstract class AppLocalizations {
/// **'Posponer otra vez'** /// **'Posponer otra vez'**
String get snoozeAgainAction; String get snoozeAgainAction;
/// No description provided for @alarmRingingNotificationTitle.
///
/// In es, this message translates to:
/// **'Alarma PluriWave'**
String get alarmRingingNotificationTitle;
/// No description provided for @alarmFireChannelName.
///
/// In es, this message translates to:
/// **'Alarmas sonando'**
String get alarmFireChannelName;
/// No description provided for @alarmFireChannelDescription.
///
/// In es, this message translates to:
/// **'Sonido y pantalla urgente cuando una alarma musical debe sonar'**
String get alarmFireChannelDescription;
/// No description provided for @alarmPreNoticeChannelName.
///
/// In es, this message translates to:
/// **'Preavisos de alarmas'**
String get alarmPreNoticeChannelName;
/// No description provided for @alarmPreNoticeChannelDescription.
///
/// In es, this message translates to:
/// **'Notificaciones silenciosas antes de la alarma'**
String get alarmPreNoticeChannelDescription;
/// No description provided for @openFolderChooserTitle.
///
/// In es, this message translates to:
/// **'Abrir carpeta'**
String get openFolderChooserTitle;
/// No description provided for @openRecordingChooserTitle.
///
/// In es, this message translates to:
/// **'Abrir grabación'**
String get openRecordingChooserTitle;
/// No description provided for @eqDeviceEditTitle. /// No description provided for @eqDeviceEditTitle.
/// ///
/// In es, this message translates to: /// In es, this message translates to:
+22
View File
@@ -1261,6 +1261,28 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get snoozeAgainAction => 'غفوة مرة أخرى'; String get snoozeAgainAction => 'غفوة مرة أخرى';
@override
String get alarmRingingNotificationTitle => 'منبّه PluriWave';
@override
String get alarmFireChannelName => 'المنبّهات الرنّانة';
@override
String get alarmFireChannelDescription =>
'صوت وشاشة عاجلة عندما يجب أن يرنّ منبّه موسيقي';
@override
String get alarmPreNoticeChannelName => 'تنبيهات المنبّهات';
@override
String get alarmPreNoticeChannelDescription => 'إشعارات صامتة قبل المنبّه';
@override
String get openFolderChooserTitle => 'فتح المجلد';
@override
String get openRecordingChooserTitle => 'فتح التسجيل';
@override @override
String get eqDeviceEditTitle => 'تعديل الجهاز'; String get eqDeviceEditTitle => 'تعديل الجهاز';
+23
View File
@@ -1268,6 +1268,29 @@ class AppLocalizationsBn extends AppLocalizations {
@override @override
String get snoozeAgainAction => 'আবার স্নুজ করুন'; String get snoozeAgainAction => 'আবার স্নুজ করুন';
@override
String get alarmRingingNotificationTitle => 'PluriWave অ্যালার্ম';
@override
String get alarmFireChannelName => 'বাজতে থাকা অ্যালার্ম';
@override
String get alarmFireChannelDescription =>
'কোনো মিউজিক অ্যালার্ম বাজার সময় জরুরি শব্দ ও স্ক্রিন';
@override
String get alarmPreNoticeChannelName => 'অ্যালার্ম রিমাইন্ডার';
@override
String get alarmPreNoticeChannelDescription =>
'অ্যালার্মের আগে নীরব বিজ্ঞপ্তি';
@override
String get openFolderChooserTitle => 'ফোল্ডার খুলুন';
@override
String get openRecordingChooserTitle => 'রেকর্ডিং খুলুন';
@override @override
String get eqDeviceEditTitle => 'ডিভাইস সম্পাদনা করুন'; String get eqDeviceEditTitle => 'ডিভাইস সম্পাদনা করুন';
+23
View File
@@ -1278,6 +1278,29 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get snoozeAgainAction => 'Erneut schlummern'; String get snoozeAgainAction => 'Erneut schlummern';
@override
String get alarmRingingNotificationTitle => 'PluriWave-Wecker';
@override
String get alarmFireChannelName => 'Klingelnde Wecker';
@override
String get alarmFireChannelDescription =>
'Dringender Ton und Bildschirm, wenn ein Musikwecker klingeln soll';
@override
String get alarmPreNoticeChannelName => 'Wecker-Vorankündigungen';
@override
String get alarmPreNoticeChannelDescription =>
'Lautlose Benachrichtigungen vor dem Wecker';
@override
String get openFolderChooserTitle => 'Ordner öffnen';
@override
String get openRecordingChooserTitle => 'Aufnahme öffnen';
@override @override
String get eqDeviceEditTitle => 'Gerät bearbeiten'; String get eqDeviceEditTitle => 'Gerät bearbeiten';
+23
View File
@@ -1264,6 +1264,29 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get snoozeAgainAction => 'Snooze again'; String get snoozeAgainAction => 'Snooze again';
@override
String get alarmRingingNotificationTitle => 'PluriWave alarm';
@override
String get alarmFireChannelName => 'Ringing alarms';
@override
String get alarmFireChannelDescription =>
'Urgent sound and screen when a music alarm must ring';
@override
String get alarmPreNoticeChannelName => 'Alarm reminders';
@override
String get alarmPreNoticeChannelDescription =>
'Silent notifications before the alarm';
@override
String get openFolderChooserTitle => 'Open folder';
@override
String get openRecordingChooserTitle => 'Open recording';
@override @override
String get eqDeviceEditTitle => 'Edit device'; String get eqDeviceEditTitle => 'Edit device';
+23
View File
@@ -1273,6 +1273,29 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get snoozeAgainAction => 'Posponer otra vez'; String get snoozeAgainAction => 'Posponer otra vez';
@override
String get alarmRingingNotificationTitle => 'Alarma PluriWave';
@override
String get alarmFireChannelName => 'Alarmas sonando';
@override
String get alarmFireChannelDescription =>
'Sonido y pantalla urgente cuando una alarma musical debe sonar';
@override
String get alarmPreNoticeChannelName => 'Preavisos de alarmas';
@override
String get alarmPreNoticeChannelDescription =>
'Notificaciones silenciosas antes de la alarma';
@override
String get openFolderChooserTitle => 'Abrir carpeta';
@override
String get openRecordingChooserTitle => 'Abrir grabación';
@override @override
String get eqDeviceEditTitle => 'Editar dispositivo'; String get eqDeviceEditTitle => 'Editar dispositivo';
+23
View File
@@ -1283,6 +1283,29 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get snoozeAgainAction => 'Reporter encore'; String get snoozeAgainAction => 'Reporter encore';
@override
String get alarmRingingNotificationTitle => 'Alarme PluriWave';
@override
String get alarmFireChannelName => 'Alarmes en cours';
@override
String get alarmFireChannelDescription =>
'Son et écran urgents lorsqu\'une alarme musicale doit sonner';
@override
String get alarmPreNoticeChannelName => 'Rappels d\'alarme';
@override
String get alarmPreNoticeChannelDescription =>
'Notifications silencieuses avant l\'alarme';
@override
String get openFolderChooserTitle => 'Ouvrir le dossier';
@override
String get openRecordingChooserTitle => 'Ouvrir l\'enregistrement';
@override @override
String get eqDeviceEditTitle => 'Modifier l\'appareil'; String get eqDeviceEditTitle => 'Modifier l\'appareil';
+22
View File
@@ -1267,6 +1267,28 @@ class AppLocalizationsHi extends AppLocalizations {
@override @override
String get snoozeAgainAction => 'फिर से स्नूज़ करें'; String get snoozeAgainAction => 'फिर से स्नूज़ करें';
@override
String get alarmRingingNotificationTitle => 'PluriWave अलार्म';
@override
String get alarmFireChannelName => 'बजते अलार्म';
@override
String get alarmFireChannelDescription =>
'जब कोई संगीत अलार्म बजना हो तो तत्काल ध्वनि और स्क्रीन';
@override
String get alarmPreNoticeChannelName => 'अलार्म रिमाइंडर';
@override
String get alarmPreNoticeChannelDescription => 'अलार्म से पहले मूक सूचनाएँ';
@override
String get openFolderChooserTitle => 'फ़ोल्डर खोलें';
@override
String get openRecordingChooserTitle => 'रिकॉर्डिंग खोलें';
@override @override
String get eqDeviceEditTitle => 'डिवाइस संपादित करें'; String get eqDeviceEditTitle => 'डिवाइस संपादित करें';
+23
View File
@@ -1272,6 +1272,29 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get snoozeAgainAction => 'Tunda lagi'; String get snoozeAgainAction => 'Tunda lagi';
@override
String get alarmRingingNotificationTitle => 'Alarm PluriWave';
@override
String get alarmFireChannelName => 'Alarm berbunyi';
@override
String get alarmFireChannelDescription =>
'Suara dan layar mendesak saat alarm musik harus berbunyi';
@override
String get alarmPreNoticeChannelName => 'Pengingat alarm';
@override
String get alarmPreNoticeChannelDescription =>
'Notifikasi senyap sebelum alarm';
@override
String get openFolderChooserTitle => 'Buka folder';
@override
String get openRecordingChooserTitle => 'Buka rekaman';
@override @override
String get eqDeviceEditTitle => 'Edit perangkat'; String get eqDeviceEditTitle => 'Edit perangkat';
+23
View File
@@ -1278,6 +1278,29 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get snoozeAgainAction => 'Posponi di nuovo'; String get snoozeAgainAction => 'Posponi di nuovo';
@override
String get alarmRingingNotificationTitle => 'Sveglia PluriWave';
@override
String get alarmFireChannelName => 'Sveglie in corso';
@override
String get alarmFireChannelDescription =>
'Suono e schermo urgenti quando una sveglia musicale deve suonare';
@override
String get alarmPreNoticeChannelName => 'Promemoria sveglia';
@override
String get alarmPreNoticeChannelDescription =>
'Notifiche silenziose prima della sveglia';
@override
String get openFolderChooserTitle => 'Apri cartella';
@override
String get openRecordingChooserTitle => 'Apri registrazione';
@override @override
String get eqDeviceEditTitle => 'Modifica dispositivo'; String get eqDeviceEditTitle => 'Modifica dispositivo';
+21
View File
@@ -1228,6 +1228,27 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get snoozeAgainAction => 'もう一度スヌーズ'; String get snoozeAgainAction => 'もう一度スヌーズ';
@override
String get alarmRingingNotificationTitle => 'PluriWave アラーム';
@override
String get alarmFireChannelName => '鳴っているアラーム';
@override
String get alarmFireChannelDescription => '音楽アラームが鳴るときの緊急の音と画面';
@override
String get alarmPreNoticeChannelName => 'アラームの事前通知';
@override
String get alarmPreNoticeChannelDescription => 'アラーム前のサイレント通知';
@override
String get openFolderChooserTitle => 'フォルダを開く';
@override
String get openRecordingChooserTitle => '録音を開く';
@override @override
String get eqDeviceEditTitle => 'デバイスを編集'; String get eqDeviceEditTitle => 'デバイスを編集';
+23
View File
@@ -1270,6 +1270,29 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get snoozeAgainAction => 'Adiar novamente'; String get snoozeAgainAction => 'Adiar novamente';
@override
String get alarmRingingNotificationTitle => 'Alarme PluriWave';
@override
String get alarmFireChannelName => 'Alarmes tocando';
@override
String get alarmFireChannelDescription =>
'Som e tela urgentes quando um alarme musical deve tocar';
@override
String get alarmPreNoticeChannelName => 'Avisos de alarme';
@override
String get alarmPreNoticeChannelDescription =>
'Notificações silenciosas antes do alarme';
@override
String get openFolderChooserTitle => 'Abrir pasta';
@override
String get openRecordingChooserTitle => 'Abrir gravação';
@override @override
String get eqDeviceEditTitle => 'Editar dispositivo'; String get eqDeviceEditTitle => 'Editar dispositivo';
+23
View File
@@ -1274,6 +1274,29 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get snoozeAgainAction => 'Отложить снова'; String get snoozeAgainAction => 'Отложить снова';
@override
String get alarmRingingNotificationTitle => 'Будильник PluriWave';
@override
String get alarmFireChannelName => 'Звонящие будильники';
@override
String get alarmFireChannelDescription =>
'Срочный звук и экран, когда должен сработать музыкальный будильник';
@override
String get alarmPreNoticeChannelName => 'Напоминания о будильнике';
@override
String get alarmPreNoticeChannelDescription =>
'Беззвучные уведомления перед будильником';
@override
String get openFolderChooserTitle => 'Открыть папку';
@override
String get openRecordingChooserTitle => 'Открыть запись';
@override @override
String get eqDeviceEditTitle => 'Изменить устройство'; String get eqDeviceEditTitle => 'Изменить устройство';
+21
View File
@@ -1221,6 +1221,27 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get snoozeAgainAction => '再次小睡'; String get snoozeAgainAction => '再次小睡';
@override
String get alarmRingingNotificationTitle => 'PluriWave 闹钟';
@override
String get alarmFireChannelName => '响铃的闹钟';
@override
String get alarmFireChannelDescription => '音乐闹钟需要响起时的紧急声音和屏幕';
@override
String get alarmPreNoticeChannelName => '闹钟预告';
@override
String get alarmPreNoticeChannelDescription => '闹钟前的静音通知';
@override
String get openFolderChooserTitle => '打开文件夹';
@override
String get openRecordingChooserTitle => '打开录音';
@override @override
String get eqDeviceEditTitle => '编辑设备'; String get eqDeviceEditTitle => '编辑设备';
+30 -22
View File
@@ -172,29 +172,41 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
@override @override
void configurarLocalizaciones(AppLocalizations l10n) { void configurarLocalizaciones(AppLocalizations l10n) {
_l10n = l10n; _l10n = l10n;
// Push every localized notification/channel/chooser string to the native
// side so it can build localized notifications even when the Flutter engine
// is dead (alarm fired from a killed app). Fire-and-forget; runs once per
// locale change (callers guard against per-rebuild churn).
unawaited(_enviarTextosNotificacion(l10n));
} }
/// Builds a pre-notice template string with a literal `{minutes}` placeholder Future<void> _enviarTextosNotificacion(AppLocalizations l10n) async {
/// for Kotlin to replace at broadcast-receiver fire time. try {
/// await _channel.invokeMethod<void>('setNotificationStrings', {
/// Strategy: call [preNoticeCountdown] with a unique sentinel integer and 'ringTitle': l10n.alarmRingingNotificationTitle,
/// replace the sentinel's string representation with `{minutes}`. 'snoozeLabel': l10n.snoozeAction,
static String _preNoticeTemplate(AppLocalizations l10n) { 'stopLabel': l10n.stopAlarmAction,
const sentinel = 42424242; 'skipLabel': l10n.skipNextAction,
return l10n.preNoticeCountdown(sentinel).replaceFirst( 'snoozeAgainLabel': l10n.snoozeAgainAction,
sentinel.toString(), 'fireChannelName': l10n.alarmFireChannelName,
'{minutes}', 'fireChannelDescription': l10n.alarmFireChannelDescription,
); 'preNoticeChannelName': l10n.alarmPreNoticeChannelName,
'preNoticeChannelDescription': l10n.alarmPreNoticeChannelDescription,
'preNoticeTemplate': _plantillaMinutos(l10n.preNoticeCountdown),
'snoozeCountdownTemplate': _plantillaMinutos(l10n.snoozeCountdown),
'openFolderTitle': l10n.openFolderChooserTitle,
'openRecordingTitle': l10n.openRecordingChooserTitle,
});
} catch (e) {
debugPrint('[PluriWave][alarmas] setNotificationStrings ERROR $e');
}
} }
/// Same sentinel strategy as [_preNoticeTemplate], but for the snooze /// Turns a localized `{int} -> String` message into a template with a literal
/// countdown notification re-posted every minute while an alarm is snoozed. /// `{minutes}` placeholder for Kotlin to fill at fire time: it calls the
static String _snoozeCountdownTemplate(AppLocalizations l10n) { /// message with a unique sentinel and swaps the sentinel back for `{minutes}`.
static String _plantillaMinutos(String Function(int) traducir) {
const sentinel = 42424242; const sentinel = 42424242;
return l10n.snoozeCountdown(sentinel).replaceFirst( return traducir(sentinel).replaceFirst(sentinel.toString(), '{minutes}');
sentinel.toString(),
'{minutes}',
);
} }
@override @override
@@ -216,10 +228,6 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
final programada = await _channel.invokeMethod<bool>('scheduleAlarm', { final programada = await _channel.invokeMethod<bool>('scheduleAlarm', {
'id': alarma.id, 'id': alarma.id,
'title': localizedAlarmName(_textos, alarma.nombre), 'title': localizedAlarmName(_textos, alarma.nombre),
'preNoticeTemplate': _preNoticeTemplate(_textos),
'snoozeCountdownTemplate': _snoozeCountdownTemplate(_textos),
'snoozeAgainLabel': _textos.snoozeAgainAction,
'snoozeStopLabel': _textos.stopAlarmAction,
'triggerAtMillis': proxima.millisecondsSinceEpoch, 'triggerAtMillis': proxima.millisecondsSinceEpoch,
'preNoticeAtMillis': 'preNoticeAtMillis':
alarma.snoozeHasta == null alarma.snoozeHasta == null
@@ -1,5 +1,8 @@
import 'dart:ui' show Locale;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/l10n/gen/app_localizations.dart';
import 'package:pluriwave/modelos/alarma_musical.dart'; import 'package:pluriwave/modelos/alarma_musical.dart';
import 'package:pluriwave/servicios/servicio_alarmas_android.dart'; import 'package:pluriwave/servicios/servicio_alarmas_android.dart';
@@ -25,55 +28,75 @@ void main() {
}); });
test( test(
'programar includes preNoticeTemplate with {minutes} placeholder in MethodChannel call', 'configurarLocalizaciones pushes localized notification strings with {minutes} templates',
() async { () async {
final servicio = ServicioAlarmasAndroid(channel: channel); final servicio = ServicioAlarmasAndroid(channel: channel);
final alarma = AlarmaMusical( final l10n = lookupAppLocalizations(const Locale('es'));
id: 'test-alarm',
nombre: 'Morning alarm', servicio.configurarLocalizaciones(l10n);
hora: 7, // configurarLocalizaciones fires setNotificationStrings as unawaited; let
minuto: 0, // the microtask/event queue drain so the MethodChannel call is recorded.
tipoProgramacion: TipoProgramacionAlarma.diaria, await Future<void>.delayed(Duration.zero);
diasSemana: const [],
proximaEjecucion: DateTime(2099, 1, 1, 7, 0), final llamada = llamadas.singleWhere(
(c) => c.method == 'setNotificationStrings',
);
final args = llamada.arguments as Map<Object?, Object?>;
expect(
args['preNoticeTemplate'],
contains('{minutes}'),
reason: 'pre-notice template must keep the {minutes} placeholder',
);
expect(
args['snoozeCountdownTemplate'],
contains('{minutes}'),
reason: 'snooze countdown template must keep the {minutes} placeholder',
); );
await servicio.programar(alarma); // Every notification/channel/chooser string must be present and non-empty
// so the native side never falls back to English for a configured locale.
final llamada = llamadas.singleWhere((c) => c.method == 'scheduleAlarm'); const claves = [
final args = llamada.arguments as Map<Object?, Object?>; 'ringTitle',
expect(args.containsKey('preNoticeTemplate'), isTrue, 'snoozeLabel',
reason: 'preNoticeTemplate must be present in scheduleAlarm args'); 'stopLabel',
final template = args['preNoticeTemplate'] as String?; 'skipLabel',
expect(template, isNotNull, 'snoozeAgainLabel',
reason: 'preNoticeTemplate must not be null'); 'fireChannelName',
expect(template, contains('{minutes}'), 'fireChannelDescription',
reason: 'preNoticeTemplate must contain the {minutes} placeholder'); 'preNoticeChannelName',
'preNoticeChannelDescription',
'openFolderTitle',
'openRecordingTitle',
];
for (final clave in claves) {
expect(args[clave], isA<String>(), reason: '$clave missing');
expect(
(args[clave] as String).isNotEmpty,
isTrue,
reason: '$clave is empty',
);
}
}, },
); );
test( test('scheduleAlarm no longer carries notification string templates', () async {
'programar preNoticeTemplate uses default locale fallback when no l10n configured', final servicio = ServicioAlarmasAndroid(channel: channel);
() async { final alarma = AlarmaMusical(
// ServicioAlarmasAndroid falls back to es locale when no l10n is configured id: 'no-template',
final servicio = ServicioAlarmasAndroid(channel: channel); nombre: 'Alarm',
final alarma = AlarmaMusical( hora: 8,
id: 'test-alarm-2', minuto: 30,
nombre: 'Alarm', tipoProgramacion: TipoProgramacionAlarma.diaria,
hora: 8, diasSemana: const [],
minuto: 30, proximaEjecucion: DateTime(2099, 1, 2, 8, 30),
tipoProgramacion: TipoProgramacionAlarma.diaria, );
diasSemana: const [],
proximaEjecucion: DateTime(2099, 1, 2, 8, 30),
);
await servicio.programar(alarma); await servicio.programar(alarma);
final llamada = llamadas.singleWhere((c) => c.method == 'scheduleAlarm'); final llamada = llamadas.singleWhere((c) => c.method == 'scheduleAlarm');
final args = llamada.arguments as Map<Object?, Object?>; final args = llamada.arguments as Map<Object?, Object?>;
final template = args['preNoticeTemplate'] as String?; expect(args.containsKey('preNoticeTemplate'), isFalse);
// The template must contain the literal placeholder string expect(args.containsKey('snoozeCountdownTemplate'), isFalse);
expect(template, contains('{minutes}')); });
},
);
} }