Adds a permanent, non-consumable premium unlock (EstadoEntitlement + PuertoCompras/ServicioComprasPlayBilling) that removes ads and unlocks alarm vacations, alarms past a 5-alarm free cap, recording start, and full Android Auto browsing. The phone equalizer stays free for everyone. - Entitlement is prefs-backed (compra_premium_v1), fail-open, and resolvable headlessly via esPremiumPersistido() for the Android Auto audio handler, which registers before runApp. - Android Auto reduced mode keeps the real root folder labels for free users; browsing into any of them (and playFromMediaId/playFromSearch/ skipToNext/skipToPrevious) is blocked at the getChildren/servicio_audio choke points, with a locked "Función Premium" item as the backstop. Current-station play/pause/stop stays untouched. A free -> premium transition actively invalidates the head unit's cached browse tree. - Ads (top banner + capped interstitial before adding a station or an alarm) are gated behind entitlement via ServicioAnuncios, using official Google test ad unit IDs pending AdMob provisioning. - Alarm cap UX shows an explanatory message with a secondary unlock action rather than a bare paywall jump; existing data is grandfathered. - 4 new localization keys translated across all 13 supported locales. Co-located tests use strict TDD (RED test before implementation) for every new pure-logic unit; full existing suite passes unchanged.
290 lines
12 KiB
Dart
290 lines
12 KiB
Dart
/// WU18 anti-copy guard allowlist (task 18.1b).
|
|
///
|
|
/// A key-parity check alone is not enough to prove a locale was actually
|
|
/// translated: it passes just as happily if a locale simply copies the
|
|
/// Spanish (`app_es.arb`, the l10n template) value verbatim. `arb_anti_copy_test.dart`
|
|
/// scans every non-`es` locale for values that are byte-identical to the
|
|
/// Spanish template and fails unless each one is listed here.
|
|
///
|
|
/// Legitimately-identical values DO exist -- brand names, international
|
|
/// loanwords, pure symbols, and words that are simply spelled the same in
|
|
/// closely related Romance languages (Spanish/Portuguese/Italian/French all
|
|
/// share a huge amount of short, common vocabulary by descent, not by
|
|
/// accident). This file is the deliberate, reviewed escape hatch for those.
|
|
///
|
|
/// Every entry below was reviewed individually and is grouped only for
|
|
/// readability -- this file must NEVER be bulk-populated to silence the
|
|
/// test. If you find an unlisted collision, translate it; do not add it
|
|
/// here unless it is genuinely untranslatable.
|
|
library;
|
|
|
|
/// (locale, ARB key) pairs whose value is intentionally identical to the
|
|
/// Spanish template's value for that key.
|
|
const Set<(String locale, String key)> identicalValueAllowlist = {
|
|
// ---------------------------------------------------------------------
|
|
// Brand / product names -- never translated in any language.
|
|
// ---------------------------------------------------------------------
|
|
('ar', 'appTitle'), // "PluriWave"
|
|
('bn', 'appTitle'),
|
|
('de', 'appTitle'),
|
|
('fr', 'appTitle'),
|
|
('hi', 'appTitle'),
|
|
('id', 'appTitle'),
|
|
('it', 'appTitle'),
|
|
('ja', 'appTitle'),
|
|
('pt', 'appTitle'),
|
|
('ru', 'appTitle'),
|
|
('zh', 'appTitle'),
|
|
('ar', 'welcomeBullet2Title'), // "Android Auto" -- Google product name
|
|
('bn', 'welcomeBullet2Title'),
|
|
('de', 'welcomeBullet2Title'),
|
|
('fr', 'welcomeBullet2Title'),
|
|
('hi', 'welcomeBullet2Title'),
|
|
('id', 'welcomeBullet2Title'),
|
|
('it', 'welcomeBullet2Title'),
|
|
('ja', 'welcomeBullet2Title'),
|
|
('pt', 'welcomeBullet2Title'),
|
|
('ru', 'welcomeBullet2Title'),
|
|
('zh', 'welcomeBullet2Title'),
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Pure symbols / placeholders -- no translatable text at all.
|
|
// ---------------------------------------------------------------------
|
|
('ar', 'dash'), // "—" (em dash)
|
|
('bn', 'dash'),
|
|
('de', 'dash'),
|
|
('fr', 'dash'),
|
|
('hi', 'dash'),
|
|
('id', 'dash'),
|
|
('it', 'dash'),
|
|
('ja', 'dash'),
|
|
('pt', 'dash'),
|
|
('ru', 'dash'),
|
|
('zh', 'dash'),
|
|
('ar', 'streamUrlHint'), // example URL shown as form hint text, not prose
|
|
('bn', 'streamUrlHint'),
|
|
('de', 'streamUrlHint'),
|
|
('fr', 'streamUrlHint'),
|
|
('hi', 'streamUrlHint'),
|
|
('id', 'streamUrlHint'),
|
|
('it', 'streamUrlHint'),
|
|
('ja', 'streamUrlHint'),
|
|
('pt', 'streamUrlHint'),
|
|
('ru', 'streamUrlHint'),
|
|
('zh', 'streamUrlHint'),
|
|
(
|
|
'ar',
|
|
'favoriteGroupsChipLabel',
|
|
), // "{groupName} · {count}" -- placeholders + separator only
|
|
('bn', 'favoriteGroupsChipLabel'),
|
|
('de', 'favoriteGroupsChipLabel'),
|
|
('fr', 'favoriteGroupsChipLabel'),
|
|
('hi', 'favoriteGroupsChipLabel'),
|
|
('id', 'favoriteGroupsChipLabel'),
|
|
('it', 'favoriteGroupsChipLabel'),
|
|
('ja', 'favoriteGroupsChipLabel'),
|
|
('pt', 'favoriteGroupsChipLabel'),
|
|
('ru', 'favoriteGroupsChipLabel'),
|
|
('zh', 'favoriteGroupsChipLabel'),
|
|
|
|
// ---------------------------------------------------------------------
|
|
// International music-genre labels -- treated as proper nouns and left
|
|
// untranslated by convention across virtually every music app/catalog
|
|
// (Spotify, Apple Music, YouTube Music, etc. all do the same).
|
|
// ---------------------------------------------------------------------
|
|
('de', 'genrePop'), ('de', 'genreRock'), ('de', 'genreJazz'),
|
|
('de', 'genreHipHop'), ('de', 'genreCountry'), ('de', 'genreMetal'),
|
|
('de', 'genreReggae'),
|
|
('fr', 'genrePop'), ('fr', 'genreRock'), ('fr', 'genreJazz'),
|
|
('fr', 'genreHipHop'), ('fr', 'genreCountry'), ('fr', 'genreMetal'),
|
|
('fr', 'genreReggae'),
|
|
('id', 'genrePop'), ('id', 'genreRock'), ('id', 'genreJazz'),
|
|
('id', 'genreHipHop'), ('id', 'genreCountry'), ('id', 'genreMetal'),
|
|
('id', 'genreReggae'),
|
|
('it', 'genrePop'), ('it', 'genreRock'), ('it', 'genreJazz'),
|
|
('it', 'genreHipHop'), ('it', 'genreCountry'), ('it', 'genreMetal'),
|
|
('it', 'genreReggae'),
|
|
('it', 'genreLatin'), // "Latina" -- also an es/it cognate
|
|
('pt', 'genrePop'), ('pt', 'genreRock'), ('pt', 'genreJazz'),
|
|
('pt', 'genreHipHop'), ('pt', 'genreCountry'), ('pt', 'genreMetal'),
|
|
('pt', 'genreReggae'),
|
|
('pt', 'genreLatin'), // "Latina" -- also an es/pt cognate
|
|
// Same genre names re-used verbatim as equalizer preset names.
|
|
('de', 'equalizerPresetRock'),
|
|
('de', 'equalizerPresetPop'),
|
|
('de', 'equalizerPresetJazz'),
|
|
('fr', 'equalizerPresetRock'),
|
|
('fr', 'equalizerPresetPop'),
|
|
('fr', 'equalizerPresetJazz'),
|
|
('id', 'equalizerPresetRock'),
|
|
('id', 'equalizerPresetPop'),
|
|
('id', 'equalizerPresetJazz'),
|
|
('it', 'equalizerPresetRock'),
|
|
('it', 'equalizerPresetPop'),
|
|
('it', 'equalizerPresetJazz'),
|
|
('pt', 'equalizerPresetRock'),
|
|
('pt', 'equalizerPresetPop'),
|
|
('pt', 'equalizerPresetJazz'),
|
|
|
|
// ---------------------------------------------------------------------
|
|
// "Preset"/"OK" -- international tech loanwords already established
|
|
// throughout this file (see `advancedEqDevicePresetLabel`, `actionOk`).
|
|
// ---------------------------------------------------------------------
|
|
('de', 'actionOk'), ('de', 'statusOk'),
|
|
('fr', 'actionOk'), ('fr', 'statusOk'),
|
|
('it', 'actionOk'), ('it', 'statusOk'),
|
|
('pt', 'actionOk'), ('pt', 'statusOk'),
|
|
('de', 'advancedEqDevicePresetLabel'), // "Preset: {presetName}"
|
|
('id', 'advancedEqDevicePresetLabel'),
|
|
('it', 'advancedEqDevicePresetLabel'),
|
|
('pt', 'advancedEqDevicePresetLabel'),
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Duration / count unit abbreviations -- "min", "h", "s" are
|
|
// internationally understood abbreviations kept literal across many
|
|
// languages in this file already (not English left in place; German,
|
|
// French, Italian and Portuguese all natively abbreviate their own words
|
|
// for minute/hour/second the same way).
|
|
// ---------------------------------------------------------------------
|
|
('fr', 'durationHoursMinutesSeconds'), ('fr', 'durationMinutesSeconds'),
|
|
('fr', 'durationMinutesOnly'), ('fr', 'durationSecondsOnly'),
|
|
('fr', 'alarmSnoozeOptionLabel'), // "{minutes} min"
|
|
('it', 'durationHoursMinutesSeconds'), ('it', 'durationMinutesSeconds'),
|
|
('it', 'durationMinutesOnly'), ('it', 'durationSecondsOnly'),
|
|
('it', 'alarmSnoozeOptionLabel'),
|
|
('pt', 'durationHoursMinutesSeconds'), ('pt', 'durationMinutesSeconds'),
|
|
('pt', 'durationMinutesOnly'), ('pt', 'durationSecondsOnly'),
|
|
('pt', 'alarmSnoozeOptionLabel'),
|
|
(
|
|
'fr',
|
|
'stationsCount',
|
|
), // "{count} radios" -- "radios" is a real French cognate plural
|
|
(
|
|
'de',
|
|
'alarmFadeInLabel',
|
|
), // "Fade-in {seconds}s" -- "fade-in" is a standard audio-production term
|
|
('it', 'alarmFadeInLabel'),
|
|
('pt', 'alarmFadeInLabel'),
|
|
(
|
|
'de',
|
|
'settingsGroupAudioTitle',
|
|
), // "AUDIO" -- shared international/Latin-root term (WU18 new key)
|
|
('fr', 'settingsGroupAudioTitle'),
|
|
('id', 'settingsGroupAudioTitle'),
|
|
('it', 'settingsGroupAudioTitle'),
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Short weekday abbreviations that happen to coincide across Romance
|
|
// languages even though the full weekday names differ (only the
|
|
// 3-letter abbreviation collides, e.g. lunes/lundi/lunedì -> "Lun").
|
|
// ---------------------------------------------------------------------
|
|
('fr', 'weekdayShortMonday'), ('fr', 'weekdayShortTuesday'),
|
|
('it', 'weekdayShortMonday'),
|
|
('it', 'weekdayShortTuesday'),
|
|
('it', 'weekdayShortSunday'),
|
|
('pt', 'weekdayShortSaturday'), ('pt', 'weekdayShortSunday'),
|
|
('fr', 'endField'), // "Fin" -- es/fr cognate ("end")
|
|
('fr', 'endLabel'), // "Fin"
|
|
// ---------------------------------------------------------------------
|
|
// Country / language endonyms and cognate proper nouns -- country and
|
|
// language names are frequently identical or near-identical across
|
|
// Romance languages (and beyond); several are also the country's own
|
|
// official native name.
|
|
// ---------------------------------------------------------------------
|
|
('id', 'countryArgentina'),
|
|
('it', 'countryArgentina'),
|
|
('pt', 'countryArgentina'),
|
|
('id', 'countryItaly'),
|
|
('it', 'countryItaly'), // "Italia" -- Italy's own endonym
|
|
('id', 'countryBrazil'),
|
|
(
|
|
'pt',
|
|
'countryBrazil',
|
|
), // "Brasil" -- Brazil's own official (Portuguese) name
|
|
('it', 'countryFrance'), // "Francia"
|
|
('pt', 'countryMexico'), // "México"
|
|
('pt', 'countryUk'), // "Reino Unido" -- identical es/pt calque
|
|
('it', 'languageSystemDefault'), // "Sistema"
|
|
('pt', 'languageSystemDefault'),
|
|
('pt', 'languageSectionTitle'), // "Idioma"
|
|
('pt', 'searchLanguageFilterLabel'), // "Idioma"
|
|
('pt', 'searchCountryFilterLabel'), // "País"
|
|
('pt', 'countryOptionalLabel'), // "País (opcional)"
|
|
// ---------------------------------------------------------------------
|
|
// Portuguese/Spanish close-cognate vocabulary. Portuguese and Spanish
|
|
// are both Iberian Romance languages and share an unusually large amount
|
|
// of short, common UI vocabulary verbatim -- these are correct,
|
|
// independently-chosen Portuguese words that happen to be spelled the
|
|
// same as their Spanish counterpart, not an untranslated leftover.
|
|
// ---------------------------------------------------------------------
|
|
('pt', 'navSearch'), // "Buscar"
|
|
('pt', 'navFavorites'), // "Favoritos"
|
|
('pt', 'favoritesTitle'), // "Favoritos"
|
|
('pt', 'cancelAction'), // "Cancelar"
|
|
('pt', 'cancelTimer'), // "Cancelar timer"
|
|
('pt', 'startTimer'), // "Iniciar timer"
|
|
('pt', 'hoursLabel'), // "Horas"
|
|
('pt', 'minutesLabel'), // "Minutos"
|
|
('pt', 'secondsLabel'), // "Segundos"
|
|
('pt', 'timeField'), // "Hora"
|
|
('pt', 'alarmInlineHourLabel'), // "Hora" (WU18 new key, task 18.3)
|
|
('it', 'alarmInlineMinuteLabel'), // "Minuto" (WU18 new key, task 18.3)
|
|
('pt', 'alarmInlineMinuteLabel'), // "Minuto" (WU18 new key, task 18.3)
|
|
('pt', 'countriesScreenTitle'), // "Países" (WU18 new key, task 18.3)
|
|
('pt', 'favoritesFilterAllLabel'), // "Todas" (WU18 new key, task 18.3)
|
|
('pt', 'seeAllAction'), // "Ver todas" (WU18 new key, task 18.3)
|
|
(
|
|
'pt',
|
|
'vacationUpcomingSectionTitle',
|
|
), // "PROGRAMADOS" (WU18 new key, task 18.3)
|
|
('pt', 'settingsSafeStatus'), // "Seguro"
|
|
('pt', 'preferredStationAutomaticFallback'), // "Fallback automático"
|
|
('pt', 'recordingsMaxSizeMbLabel'), // "Megabytes máximos"
|
|
('pt', 'favoriteGroupsTitle'), // "Listas de favoritos"
|
|
('pt', 'favoriteGroupsEdit'), // "Editar lista"
|
|
('pt', 'searchFiltersLabel'), // "Filtros"
|
|
('pt', 'detectAction'), // "Detectar"
|
|
('pt', 'alarmScreenTitle'), // "Despertar musical"
|
|
('pt', 'editAction'), // "Editar"
|
|
('pt', 'defaultAlarmName'), // "Despertador musical"
|
|
('pt', 'soundDigitalPulse'), // "Pulso digital"
|
|
('pt', 'pauseAction'), // "Pausar"
|
|
('pt', 'playbackStatusConnecting'), // "Conectando..."
|
|
('pt', 'playbackStatusPaused'), // "Pausado"
|
|
('pt', 'playbackStatusReconnecting'), // "Reconectando..."
|
|
('it', 'equalizerBandLabel'), // "Banda {band}"
|
|
('pt', 'equalizerBandLabel'), // "Banda {band}"
|
|
('pt', 'equalizerPresetFlat'), // "Plano"
|
|
('pt', 'equalizerPresetVoice'), // "Voz"
|
|
('pt', 'equalizerPresetCustom'), // "Personalizado"
|
|
('pt', 'customOption'), // "Personalizada"
|
|
('pt', 'indefiniteOption'), // "Indefinida"
|
|
('pt', 'invalidNumber'), // "Número inválido"
|
|
('pt', 'stopAction'), // "Parar"
|
|
('pt', 'eqDeviceEditTitle'), // "Editar dispositivo"
|
|
('pt', 'eqDeviceConnected'), // "Conectado"
|
|
('pt', 'localMusicSectionTitle'), // "Música local (Android Auto)"
|
|
(
|
|
'pt',
|
|
'recordingsLibraryStorageCaption',
|
|
), // WU18 new key (task 18.3) -- "usados"/"MB"/"de" all coincide
|
|
(
|
|
'pt',
|
|
'searchResultsCount',
|
|
), // WU18 new key (task 18.3) -- "resultado(s)" is an es/pt cognate
|
|
(
|
|
'pt',
|
|
'alarmDiagnosticsManufacturerLabel',
|
|
), // fix/alarmas-fiabilidad new key -- "Fabricante" is identical in pt/es
|
|
(
|
|
'pt',
|
|
'desbloquearPremium',
|
|
), // iap-freemium-unlock new key -- "Desbloquear" is identical in pt/es and
|
|
// "Premium" is an untranslated product tier name in both.
|
|
(
|
|
'pt',
|
|
'restaurarCompras',
|
|
), // iap-freemium-unlock new key -- "Restaurar compras" is the standard
|
|
// Portuguese store wording and coincides with es word for word.
|
|
};
|