localization

This commit is contained in:
rhunk 2023-05-16 01:35:16 +02:00
parent 5df7d067ac
commit 185fee2027
15 changed files with 200 additions and 135 deletions

View File

@ -0,0 +1,76 @@
{
"category": {
"general": "General",
"spy": "Spy",
"media_download": "Media Downloader",
"privacy": "Privacy",
"ui": "UI",
"extras": "Extras",
"tweaks": "Tweaks",
"experimental": "Experimental"
},
"property": {
"save_folder": "Save Folder",
"prevent_read_receipts": "Prevent Read Receipts",
"hide_bitmoji_presence": "Hide Bitmoji Presence",
"show_message_content": "Show Message Content",
"message_logger": "Message Logger",
"auto_download_snaps": "Auto Download Snaps",
"auto_download_stories": "Auto Download Stories",
"auto_download_public_stories": "Auto Download Public Stories",
"auto_download_spotlight": "Auto Download Spotlight",
"overlay_merge": "Snap Image Overlay Merge",
"download_inchat_snaps": "Download Inchat Snaps",
"anti_auto_download_button": "Anti Auto Download Button",
"disable_metrics": "Disable Metrics",
"prevent_screenshots": "Prevent Screenshots",
"prevent_status_notifications": "Prevent Status Notifications (Screenrecord, media save)",
"anonymous_story_view": "Anonymous Story View",
"hide_typing_notification": "Hide Typing Notification",
"menu_slot_id": "Friend Menu Slot ID",
"message_preview_length": "Message Preview Length",
"auto_save": "Auto Save",
"snapchat_plus": "Snapchat Plus",
"remove_voice_record_button": "Remove Voice Record Button",
"remove_stickers_button": "Remove Stickers Button",
"remove_cognac_button": "Remove Cognac Button",
"remove_call_buttons": "Remove Call Buttons",
"long_snap_sending": "Long Snap Sending",
"block_ads": "Block Ads",
"streak_expiration_info": "Show Streak Expiration Info",
"new_map_ui": "New Map UI",
"use_download_manager": "Use Android Download Manager"
},
"friend_menu_option": {
"preview": "Preview",
"stealth_mode": "Stealth Mode",
"anti_auto_download": "Anti Auto Download"
},
"message_context_menu_option": {
"download": "Download",
"preview": "Preview"
},
"modal_option": {
"profile_info": "Profile Info",
"close": "Close"
},
"conversation_preview": {
"streak_expiration": "expires in %s days %s hours %s minutes",
"title": "Preview",
"unknown_user": "Unknown User"
},
"profile_info": {
"title": "Profile Info",
"username": "Username",
"display_name": "Display Name",
"added_date": "Added Date",
"birthday": "Birthday : {month} {day}"
}
}

View File

@ -89,6 +89,12 @@ class ModContext {
exitProcess(0)
}
fun crash(message: String, throwable: Throwable? = null) {
Logger.xposedLog(message, throwable)
longToast(message)
delayForceCloseApp(100)
}
fun delayForceCloseApp(delay: Long) = Handler(Looper.getMainLooper()).postDelayed({
forceCloseApp()
}, delay)

View File

@ -53,11 +53,12 @@ class BridgeClient(
BridgeMessageType.FILE_ACCESS_RESULT -> FileAccessResult()
BridgeMessageType.DOWNLOAD_CONTENT_RESULT -> DownloadContentResult()
BridgeMessageType.MESSAGE_LOGGER_RESULT -> MessageLoggerResult()
BridgeMessageType.LOCALE_RESULT -> LocaleResult()
else -> {
log("Unknown message type: ${msg.what}")
null
future.completeExceptionally(IllegalStateException("Unknown message type: ${msg.what}"))
return
}
}
} ?: return
with(message) {
read(msg.data)
@ -220,6 +221,16 @@ class BridgeClient(
)
}
fun fetchTranslations(): LocaleResult {
sendMessage(
BridgeMessageType.LOCALE_REQUEST,
LocaleRequest(),
LocaleResult::class
).run {
return this
}
}
override fun onServiceConnected(name: ComponentName, service: IBinder) {
messenger = Messenger(service)
future.complete(true)

View File

@ -3,15 +3,10 @@ package me.rhunk.snapenhance.bridge.common.impl
import android.os.Bundle
import me.rhunk.snapenhance.bridge.common.BridgeMessage
class LocaleRequest(
var locale: String? = null
) : BridgeMessage() {
class LocaleRequest() : BridgeMessage() {
override fun write(bundle: Bundle) {
bundle.putString("locale", locale)
}
override fun read(bundle: Bundle) {
locale = bundle.getString("locale")
}
}

View File

@ -10,6 +10,7 @@ import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.bridge.common.BridgeMessageType
import me.rhunk.snapenhance.bridge.common.impl.*
import java.io.File
import java.util.*
class BridgeService : Service() {
companion object {
@ -112,9 +113,13 @@ class BridgeService : Service() {
}
private fun handleLocaleRequest(msg: LocaleRequest, reply: (Message) -> Unit) {
val locale = resources.configuration.locales[0]
Logger.log("Locale: ${locale.language}_${locale.country}")
TODO()
val deviceLocale = Locale.getDefault().language
val compatibleLocale = resources.assets.list("lang")?.find { it.startsWith(deviceLocale) }?.substring(0, 2) ?: "en"
resources.assets.open("lang/$compatibleLocale.json").use { inputStream ->
val json = inputStream.bufferedReader().use { it.readText() }
reply(LocaleResult(compatibleLocale, json.toByteArray(Charsets.UTF_8)).toMessage(BridgeMessageType.LOCALE_RESULT.value))
}
}
private fun handleDownloadContent(msg: DownloadContentRequest, reply: (Message) -> Unit) {

View File

@ -3,12 +3,12 @@ package me.rhunk.snapenhance.config
enum class ConfigCategory(
val key: String
) {
GENERAL("general"),
SPY("spy"),
MEDIA_DOWNLOADER("media_download"),
PRIVACY("privacy"),
UI("ui"),
EXTRAS("extras"),
TWEAKS("tweaks"),
EXPERIMENTS("experiments");
GENERAL("category.general"),
SPY("category.spy"),
MEDIA_DOWNLOADER("category.media_download"),
PRIVACY("category.privacy"),
UI("category.ui"),
EXTRAS("category.extras"),
TWEAKS("category.tweaks"),
EXPERIMENTAL("category.experimental");
}

View File

@ -10,7 +10,7 @@ enum class ConfigProperty(
val defaultValue: Any
) {
SAVE_FOLDER(
"save_folder", "description.save_folder", ConfigCategory.GENERAL,
"property.save_folder", "description.save_folder", ConfigCategory.GENERAL,
File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).absolutePath + "/Snapchat",
"SnapEnhance"
@ -18,166 +18,148 @@ enum class ConfigProperty(
),
PREVENT_READ_RECEIPTS(
"prevent_read_receipts",
"property.prevent_read_receipts",
"description.prevent_read_receipts",
ConfigCategory.SPY,
false
),
HIDE_BITMOJI_PRESENCE(
"hide_bitmoji_presence",
"property.hide_bitmoji_presence",
"description.hide_bitmoji_presence",
ConfigCategory.SPY,
false
),
SHOW_MESSAGE_CONTENT(
"show_message_content",
"property.show_message_content",
"description.show_message_content",
ConfigCategory.SPY,
false
),
MESSAGE_LOGGER("message_logger", "description.message_logger", ConfigCategory.SPY, false),
MESSAGE_LOGGER("property.message_logger", "description.message_logger", ConfigCategory.SPY, false),
MEDIA_DOWNLOADER(
"media_downloader",
"description.media_downloader",
ConfigCategory.MEDIA_DOWNLOADER,
true
),
AUTO_DOWNLOAD_SNAPS(
"auto_download_snaps",
"property.auto_download_snaps",
"description.auto_download_snaps",
ConfigCategory.MEDIA_DOWNLOADER,
true
),
AUTO_DOWNLOAD_STORIES(
"auto_download_stories",
"property.auto_download_stories",
"description.auto_download_stories",
ConfigCategory.MEDIA_DOWNLOADER,
false
),
AUTO_DOWNLOAD_PUBLIC_STORIES(
"auto_download_public_stories",
"property.auto_download_public_stories",
"description.auto_download_public_stories",
ConfigCategory.MEDIA_DOWNLOADER,
false
),
AUTO_DOWNLOAD_SPOTLIGHT(
"auto_download_spotlight",
"property.auto_download_spotlight",
"description.auto_download_spotlight",
ConfigCategory.MEDIA_DOWNLOADER,
false
),
OVERLAY_MERGE(
"overlay_merge",
"property.overlay_merge",
"description.overlay_merge",
ConfigCategory.MEDIA_DOWNLOADER,
true
),
DOWNLOAD_INCHAT_SNAPS(
"download_inchat_snaps",
"property.download_inchat_snaps",
"description.download_inchat_snaps",
ConfigCategory.MEDIA_DOWNLOADER,
true
),
ANTI_DOWNLOAD_BUTTON(
"anti_download_button",
"description.anti_download_button",
"property.anti_auto_download_button",
"description.anti_auto_download_button",
ConfigCategory.MEDIA_DOWNLOADER,
false
),
DISABLE_METRICS("disable_metrics", "description.disable_metrics", ConfigCategory.PRIVACY, true),
DISABLE_METRICS("property.disable_metrics", "description.disable_metrics", ConfigCategory.PRIVACY, true),
PREVENT_SCREENSHOTS(
"prevent_screenshots",
"property.prevent_screenshots",
"description.prevent_screenshots",
ConfigCategory.PRIVACY,
true
),
PREVENT_STATUS_NOTIFICATIONS(
"prevent_status_notifications",
"property.prevent_status_notifications",
"description.prevent_status_notifications",
ConfigCategory.PRIVACY,
true
),
ANONYMOUS_STORY_VIEW(
"anonymous_story_view",
"property.anonymous_story_view",
"description.anonymous_story_view",
ConfigCategory.PRIVACY,
false
),
HIDE_TYPING_NOTIFICATION(
"hide_typing_notification",
"property.hide_typing_notification",
"description.hide_typing_notification",
ConfigCategory.PRIVACY,
false
),
MENU_SLOT_ID("menu_slot_id", "description.menu_slot_id", ConfigCategory.UI, 1),
MENU_SLOT_ID("property.menu_slot_id", "description.menu_slot_id", ConfigCategory.UI, 1),
MESSAGE_PREVIEW_LENGTH(
"message_preview_length",
"property.message_preview_length",
"description.message_preview_length",
ConfigCategory.UI,
20
),
AUTO_SAVE("auto_save", "description.auto_save", ConfigCategory.EXTRAS, false),
/*EXTERNAL_MEDIA_AS_SNAP(
"external_media_as_snap",
"description.external_media_as_snap",
ConfigCategory.EXTRAS,
false
),
CONVERSATION_EXPORT(
"conversation_export",
"description.conversation_export",
ConfigCategory.EXTRAS,
false
),*/
SNAPCHAT_PLUS("snapchat_plus", "description.snapchat_plus", ConfigCategory.EXTRAS, false),
AUTO_SAVE("property.auto_save", "description.auto_save", ConfigCategory.EXTRAS, false),
SNAPCHAT_PLUS("property.snapchat_plus", "description.snapchat_plus", ConfigCategory.EXTRAS, false),
REMOVE_VOICE_RECORD_BUTTON(
"remove_voice_record_button",
"property.remove_voice_record_button",
"description.remove_voice_record_button",
ConfigCategory.TWEAKS,
false
),
REMOVE_STICKERS_BUTTON(
"remove_stickers_button",
"property.remove_stickers_button",
"description.remove_stickers_button",
ConfigCategory.TWEAKS,
false
),
REMOVE_COGNAC_BUTTON(
"remove_cognac_button",
"property.remove_cognac_button",
"description.remove_cognac_button",
ConfigCategory.TWEAKS,
false
),
REMOVE_CALLBUTTONS(
"remove_callbuttons",
"description.remove_callbuttons",
REMOVE_CALL_BUTTONS(
"property.remove_call_buttons",
"description.remove_call_buttons",
ConfigCategory.TWEAKS,
false
),
LONG_SNAP_SENDING(
"long_snap_sending",
"property.long_snap_sending",
"description.long_snap_sending",
ConfigCategory.TWEAKS,
false
),
BLOCK_ADS("block_ads", "description.block_ads", ConfigCategory.TWEAKS, false),
STREAKEXPIRATIONINFO(
"streakexpirationinfo",
BLOCK_ADS("property.block_ads", "description.block_ads", ConfigCategory.TWEAKS, false),
STREAK_EXPIRATION_INFO(
"property.streak_expiration_info",
"description.streakexpirationinfo",
ConfigCategory.TWEAKS,
false
),
NEW_MAP_UI("new_map_ui", "description.new_map_ui", ConfigCategory.TWEAKS, false),
NEW_MAP_UI("property.new_map_ui", "description.new_map_ui", ConfigCategory.TWEAKS, false),
USE_DOWNLOAD_MANAGER(
"use_download_manager",
"property.use_download_manager",
"description.use_download_manager",
ConfigCategory.EXPERIMENTS,
ConfigCategory.EXPERIMENTAL,
false
);

View File

@ -47,7 +47,7 @@ class ConfigEnumKeys : Feature("Config enum keys", loadParams = FeatureLoadParam
}
}
if (context.config.bool(ConfigProperty.STREAKEXPIRATIONINFO)) {
if (context.config.bool(ConfigProperty.STREAK_EXPIRATION_INFO)) {
hookAllEnums(context.mappings.getMappedClass("enums", "FRIENDS_FEED")) { key, atomicValue ->
if (key == "STREAK_EXPIRATION_INFO") atomicValue.set(true)
}

View File

@ -84,7 +84,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
)
onDownloadComplete()
} catch (e: Throwable) {
Logger.xposedLog(e)
xposedLog(e)
context.longToast("Failed to save file: " + e.message)
return false
}
@ -135,7 +135,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
if (fFmpegSession.returnCode.value != 0) {
mergedFile.delete()
context.longToast("Failed to merge video and overlay. See logs for more details.")
Logger.xposedLog(fFmpegSession.output)
xposedLog(fFmpegSession.output)
return null
}
val mergedFileData: ByteArray = FileInputStream(mergedFile).readBytes()
@ -285,11 +285,18 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
}
}
private fun canAutoDownload(): Boolean {
return context.config.bool(ConfigProperty.AUTO_DOWNLOAD_SNAPS) ||
context.config.bool(ConfigProperty.AUTO_DOWNLOAD_STORIES) ||
context.config.bool(ConfigProperty.AUTO_DOWNLOAD_PUBLIC_STORIES) ||
context.config.bool(ConfigProperty.AUTO_DOWNLOAD_SPOTLIGHT)
}
override fun asyncOnActivityCreate() {
val operaViewerControllerClass: Class<*> = context.mappings.getMappedClass("OperaPageViewController", "Class")
val onOperaViewStateCallback: (HookAdapter) -> Unit = onOperaViewStateCallback@{ param ->
if (!context.config.bool(ConfigProperty.MEDIA_DOWNLOADER)) return@onOperaViewStateCallback
if (!canAutoDownload()) return@onOperaViewStateCallback
val viewState = (param.thisObject() as Any).getObjectField(context.mappings.getMappedValue("OperaPageViewController", "viewStateField")).toString()
if (viewState != "FULLY_DISPLAYED") {
@ -317,7 +324,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
try {
handleOperaMedia(mediaParamMap, mediaInfoMap, false)
} catch (e: Throwable) {
Logger.xposedLog(e)
xposedLog(e)
context.longToast(e.message!!)
}
}
@ -390,7 +397,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
}
}
if (videoData == null || overlayData == null) {
Logger.xposedLog("Invalid data in zip file")
xposedLog("Invalid data in zip file")
return
}
val mergedVideo = mergeOverlay(videoData, overlayData, isPreviewMode)

View File

@ -45,7 +45,7 @@ class UITweaks : Feature("UITweaks", loadParams = FeatureLoadParams.ACTIVITY_CRE
if (chatInputBarSticker == viewId && context.config.bool(ConfigProperty.REMOVE_STICKERS_BUTTON)) {
view.visibility = View.GONE
}
if (context.config.bool(ConfigProperty.REMOVE_CALLBUTTONS)) {
if (context.config.bool(ConfigProperty.REMOVE_CALL_BUTTONS)) {
if (viewId == callButton1 || viewId == callButton2) {
if (view.visibility == View.GONE) return@hook
Hooker.ephemeralHookObjectMethod(View::class.java, view, "setVisibility", HookStage.BEFORE) { param ->

View File

@ -64,15 +64,15 @@ class FriendFeedInfoMenu : AbstractMenu() {
val birthday = Calendar.getInstance()
birthday[Calendar.MONTH] = (profile.birthday shr 32).toInt() - 1
val message: String = """
${context.translation.get("info.username")}: ${profile.username}
${context.translation.get("info.display_name")}: ${profile.displayName}
${context.translation.get("info.added_date")}: ${formatDate(addedTimestamp)}
${context.translation.get("profile_info.username")}: ${profile.username}
${context.translation.get("profile_info.display_name")}: ${profile.displayName}
${context.translation.get("profile_info.added_date")}: ${formatDate(addedTimestamp)}
${birthday.getDisplayName(
Calendar.MONTH,
Calendar.LONG,
context.translation.getLocale()
context.translation.locale
)?.let {
context.translation.get("info.birthday")
context.translation.get("profile_info.birthday")
.replace("{month}", it)
.replace("{day}", profile.birthday.toInt().toString())
}
@ -120,7 +120,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
}
}
var displayUsername = sender?.displayName ?: sender?.usernameForSorting?: "Unknown user"
var displayUsername = sender?.displayName ?: sender?.usernameForSorting?: context.translation.get("conversation_preview.unknown_user")
if (displayUsername.length > 12) {
displayUsername = displayUsername.substring(0, 13) + "... "
@ -136,7 +136,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
val timeSecondDiff = ((it.streakExpirationTimestamp - System.currentTimeMillis()) / 1000 / 60).toInt()
messageBuilder.append("\n\n")
.append("\uD83D\uDD25 ") //fire emoji
.append(context.translation.get("streak_expiration").format(
.append(context.translation.get("conversation_preview.streak_expiration").format(
timeSecondDiff / 60 / 24,
timeSecondDiff / 60 % 24,
timeSecondDiff % 60
@ -145,13 +145,13 @@ class FriendFeedInfoMenu : AbstractMenu() {
//alert dialog
val builder = AlertDialog.Builder(context.mainActivity)
builder.setTitle(context.translation.get("preview"))
builder.setTitle(context.translation.get("conversation_preview.title"))
builder.setMessage(messageBuilder.toString())
builder.setPositiveButton(
"OK"
) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
targetPerson?.let {
builder.setNegativeButton(context.translation.get("profile_info")) {_, _ ->
builder.setNegativeButton(context.translation.get("modal_option.profile_info")) {_, _ ->
context.executeAsync {
showProfileInfo(it)
}
@ -175,7 +175,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
//preview button
val previewButton = Button(viewModel.context)
previewButton.text = context.translation.get("preview")
previewButton.text = context.translation.get("friend_menu_option.preview")
applyTheme(viewModel, previewButton)
val finalFocusedConversationTargetUser = focusedConversationTargetUser
previewButton.setOnClickListener { v: View? ->
@ -188,7 +188,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
//stealth switch
val stealthSwitch = Switch(viewModel.context)
stealthSwitch.text = context.translation.get("stealth_mode")
stealthSwitch.text = context.translation.get("friend_menu_option.stealth_mode")
stealthSwitch.isChecked = context.feature(StealthMode::class).isStealth(conversationId)
applyTheme(viewModel, stealthSwitch)
stealthSwitch.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
@ -202,7 +202,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
val userId = context.database.getFriendFeedInfoByConversationId(conversationId)?.friendUserId ?: return
val antiAutoDownload = Switch(viewModel.context)
antiAutoDownload.text = context.translation.get("anti_auto_download")
antiAutoDownload.text = context.translation.get("friend_menu_option.anti_auto_download")
antiAutoDownload.isChecked = context.feature(AntiAutoDownload::class).isUserIgnored(userId)
applyTheme(viewModel, antiAutoDownload)
antiAutoDownload.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->

View File

@ -44,9 +44,7 @@ class MappingManager(private val context: ModContext) : Manager {
runCatching {
loadCached()
}.onFailure {
context.shortToast("Failed to load cached mappings ${it.message}")
Logger.xposedLog(it)
context.delayForceCloseApp(1000)
context.crash("Failed to load cached mappings ${it.message}", it)
}
if (snapBuildNumber != currentBuildNumber) {
@ -60,9 +58,7 @@ class MappingManager(private val context: ModContext) : Manager {
}.onSuccess {
context.shortToast("Generated mappings for build $snapBuildNumber")
}.onFailure {
context.shortToast("Failed to generate mappings ${it.message}")
Logger.xposedLog(it)
context.delayForceCloseApp(1000)
context.crash("Failed to generate mappings ${it.message}", it)
}
}

View File

@ -1,5 +1,7 @@
package me.rhunk.snapenhance.manager.impl
import com.google.gson.JsonParser
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.ModContext
import me.rhunk.snapenhance.manager.Manager
import java.util.*
@ -7,13 +9,32 @@ import java.util.*
class TranslationManager(
private val context: ModContext
) : Manager {
override fun init() {
private val translationMap = mutableMapOf<String, String>()
lateinit var locale: Locale
override fun init() {
val messageLocaleResult = context.bridgeClient.fetchTranslations();
locale = Locale(messageLocaleResult.locale!!)
val translations = JsonParser.parseString(messageLocaleResult.content?.toString(Charsets.UTF_8)).asJsonObject
if (translations == null || translations.isJsonNull) {
context.crash("Failed to fetch translations")
return
}
translations.asJsonObject.entrySet().forEach {
if (it.value.isJsonPrimitive) {
translationMap[it.key] = it.value.asString
}
if (!it.value.isJsonObject) return@forEach
it.value.asJsonObject.entrySet().forEach { entry ->
translationMap["${it.key}.${entry.key}"] = entry.value.asString
}
}
}
fun getLocale(): Locale = Locale.getDefault()
fun get(key: String): String {
return key
return translationMap[key] ?: key.also { Logger.xposedLog("Missing translation for $key") }
}
}

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@ -1,33 +1,3 @@
<resources>
<string name="app_name" translatable="false">Snap Enhance</string>
<string name="property.save_folder">Save folder</string>
<string name="property.prevent_read_receipts">Prevent read receipts</string>
<string name="property.hide_bitmoji_presence">Hide Bitmoji presence</string>
<string name="property.show_message_content">Show message content</string>
<string name="property.message_logger">Message logger</string>
<string name="property.media_downloader_feature">Media downloader feature</string>
<string name="property.download_stories">Download stories</string>
<string name="property.download_public_stories">Download public stories</string>
<string name="property.download_spotlight">Download spotlight</string>
<string name="property.overlay_merge">Overlay merge</string>
<string name="property.download_inchat_snaps">Download in chat snaps</string>
<string name="property.disable_metrics">Disable metrics</string>
<string name="property.prevent_screenshot">Prevent screenshot</string>
<string name="property.anonymous_story_view">Anonymous story view</string>
<string name="property.hide_typing_notification">Hide typing notification</string>
<string name="property.menu_slot_id">Menu slot id</string>
<string name="property.message_preview_length">Message preview length</string>
<string name="property.auto_save">Auto save</string>
<string name="property.external_media_as_snap">External media as snap</string>
<string name="property.conversation_export">Conversation export</string>
<string name="property.snapchat_plus">Snapchat Plus</string>
<string name="property.remove_voice_record_button">Remove voice record button</string>
<string name="property.remove_stickers_button">Remove stickers button</string>
<string name="property.remove_cognac_button">Remove cognac button</string>
<string name="property.remove_callbuttons">Remove call buttons</string>
<string name="property.long_snap_sending">Long snap sending</string>
<string name="property.block_ads">Block ads</string>
<string name="property.streakexpirationinfo">Streak Expiration Info</string>
<string name="property.new_map_ui">New map ui</string>
<string name="property.use_download_manager">Use download manager</string>
</resources>