diff --git a/.claude/.DS_Store b/.claude/.DS_Store new file mode 100644 index 0000000..e6c3e18 Binary files /dev/null and b/.claude/.DS_Store differ diff --git a/.claude/commands/commit-push-pr.md b/.claude/commands/commit-push-pr.md new file mode 100644 index 0000000..69009d5 --- /dev/null +++ b/.claude/commands/commit-push-pr.md @@ -0,0 +1,15 @@ +--- +allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*) +description: Create a git commit +--- + +## Context + +- Current git status: !`git status` +- Current git diff (staged and unstaged changes): !`git diff HEAD` +- Current branch: !`git branch --show-current` +- Recent commits: !`git log --oneline -10` + +## Your task + +Based on the above changes, create a single git commit. Then push it. diff --git a/.claude/commands/new-version.md b/.claude/commands/new-version.md new file mode 100644 index 0000000..d52b9fa --- /dev/null +++ b/.claude/commands/new-version.md @@ -0,0 +1,15 @@ +--- +allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*) +description: Do migration tests first. Then: update changelog, update readme, update version in gradle files, create a git commit and push it +--- + +## Context + +- Current git status: !`git status` +- Current git diff (staged and unstaged changes): !`git diff HEAD` +- Current branch: !`git branch --show-current` +- Recent commits: !`git log --oneline -10` + +## Your task + +Do migration tests first. Then: update changelog, update readme, update version in gradle files, create a git commit and push it diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3578034 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,84 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands + +```bash +# Build debug APK +./gradlew assembleDebug + +# Build release APK (with ProGuard/R8 minification) +./gradlew assembleRelease + +# Run unit tests +./gradlew test + +# Run instrumented tests (requires emulator/device) +./gradlew connectedAndroidTest + +# Clean build +./gradlew clean +``` + +APK outputs: `app/build/outputs/apk/` + +## Project Configuration + +- **SDK**: Compile/Target 34, Min 26 +- **JDK**: 17 +- **Kotlin**: 1.9.22 +- **Package**: `com.fastmask` + +## Architecture + +Clean Architecture with MVVM pattern. Three distinct layers: + +### Data Layer (`data/`) +- `api/` - JMAP protocol integration (Fastmail's native API) + - `JmapApi.kt` - API client with session caching + - `JmapService.kt` - Retrofit service interface + - `JmapModels.kt` - Kotlinx Serialization models +- `local/` - Persistence + - `TokenStorage.kt` - EncryptedSharedPreferences (lazy-initialized for Hilt compatibility) + - `SettingsDataStore.kt` - DataStore for language preferences +- `repository/` - Repository implementations + +### Domain Layer (`domain/`) +- `model/` - Domain models (`MaskedEmail`, `Language`, `EmailState` enum) +- `repository/` - Abstract interfaces +- `usecase/` - Business logic (Login, Logout, CRUD for masked emails, language settings) + +### UI Layer (`ui/`) +- `auth/`, `list/`, `create/`, `detail/`, `settings/` - Feature screens with ViewModels +- `components/` - Reusable composables (MaskedEmailCard, ShimmerEffect, ErrorMessage) +- `navigation/` - Jetpack Navigation with shared element transitions +- `theme/` - Material 3 theming with dynamic colors + +## Key Patterns + +**State Management**: `StateFlow` for reactive state, `SharedFlow` for one-time events (navigation, logout) + +**Dependency Injection**: Hilt with `NetworkModule` (Retrofit, OkHttp, Json) and `RepositoryModule` (repository bindings) + +**API Protocol**: JMAP (JSON Mail Access Protocol) with Bearer token auth. Session (accountId, apiUrl) is cached after first call. + +**Security**: Tokens stored in EncryptedSharedPreferences using Android Security Crypto library. TokenStorage uses lazy initialization to prevent Hilt injection issues. + +## Localization + +20 languages supported. String resources in `res/values-*/strings.xml`. In-app language override uses AppCompatDelegate for runtime switching without restart. + +## ProGuard/R8 + +Release builds use minification. Key rules in `app/proguard-rules.pro`: +- Keep Kotlinx Serialization and JMAP models +- Keep Google Tink classes (security-crypto dependency) +- Retrofit and OkHttp configurations + +## Commit Message Format + +- `Add: new feature description` +- `Fix: bug description` +- `Update: what was changed` +- `Refactor: what was refactored` diff --git a/README.md b/README.md index 923f6b2..47967d7 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ FastMask is a native Android application that lets you manage your [Fastmail](ht | **Delete** | Remove masks you no longer need | | **Search & Filter** | Find specific masks instantly | | **Material You** | Dynamic theming that adapts to your wallpaper | +| **20 Languages** | Full localization with in-app language picker | +| **Settings** | Language selection, contact/feedback, and logout | ## Screenshots @@ -151,6 +153,7 @@ app/ ├── list/ # Masked email list ├── create/ # Create new mask ├── detail/ # View/edit mask details + ├── settings/ # Settings screen ├── components/ # Reusable UI components ├── navigation/ # Navigation setup └── theme/ # Material 3 theming @@ -188,6 +191,12 @@ Contributions are welcome! Here's how you can help: ## Changelog +### v1.3 (January 2026) +- **New**: Settings screen with language picker, contact button, and logout +- **New**: Localization support for 20 languages (English, Chinese, Spanish, Hindi, Arabic, Portuguese, Bengali, Russian, Japanese, French, German, Korean, Italian, Turkish, Vietnamese, Polish, Ukrainian, Dutch, Thai, Indonesian) +- **New**: In-app language override using AppCompatDelegate +- **Improved**: All UI strings now use centralized string resources + ### v1.2 (January 2026) - **Fixed**: Login crash caused by `ParameterizedType` casting error at runtime - **Improved**: TokenStorage now uses lazy initialization for EncryptedSharedPreferences @@ -207,11 +216,10 @@ Contributions are welcome! Here's how you can help: ## Roadmap - [ ] Add screenshots to README -- [ ] Biometric authentication option - [ ] Widget for quick mask creation -- [ ] Export/import functionality - [ ] Dark/light mode toggle -- [ ] Localization support +- [x] Localization support (20 languages) +- [x] Settings screen with language picker ## License diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 90a0695..7dbe926 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,6 +54,7 @@ android { buildFeatures { compose = true + buildConfig = true } composeOptions { @@ -73,6 +74,7 @@ dependencies { implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") implementation("androidx.activity:activity-compose:1.9.0") + implementation("androidx.appcompat:appcompat:1.7.0") // Compose implementation(platform("androidx.compose:compose-bom:2024.09.00")) diff --git a/app/src/main/java/com/fastmask/MainActivity.kt b/app/src/main/java/com/fastmask/MainActivity.kt index d31fe89..f0da3fd 100644 --- a/app/src/main/java/com/fastmask/MainActivity.kt +++ b/app/src/main/java/com/fastmask/MainActivity.kt @@ -1,15 +1,18 @@ package com.fastmask import android.os.Bundle -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier +import androidx.core.os.LocaleListCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.navigation.compose.rememberNavController +import com.fastmask.data.local.SettingsDataStore import com.fastmask.domain.repository.AuthRepository import com.fastmask.ui.navigation.FastMaskNavHost import com.fastmask.ui.navigation.NavRoutes @@ -18,14 +21,20 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint -class MainActivity : ComponentActivity() { +class MainActivity : AppCompatActivity() { @Inject lateinit var authRepository: AuthRepository + @Inject + lateinit var settingsDataStore: SettingsDataStore + private var isReady = false override fun onCreate(savedInstanceState: Bundle?) { + // Restore saved language before anything else + restoreSavedLanguage() + val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) @@ -56,4 +65,12 @@ class MainActivity : ComponentActivity() { } } } + + private fun restoreSavedLanguage() { + val savedLanguageCode = settingsDataStore.getLanguageBlocking() + if (savedLanguageCode != null) { + val localeList = LocaleListCompat.forLanguageTags(savedLanguageCode) + AppCompatDelegate.setApplicationLocales(localeList) + } + } } diff --git a/app/src/main/java/com/fastmask/data/local/SettingsDataStore.kt b/app/src/main/java/com/fastmask/data/local/SettingsDataStore.kt new file mode 100644 index 0000000..85edc60 --- /dev/null +++ b/app/src/main/java/com/fastmask/data/local/SettingsDataStore.kt @@ -0,0 +1,44 @@ +package com.fastmask.data.local + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking +import javax.inject.Inject +import javax.inject.Singleton + +private val Context.dataStore: DataStore by preferencesDataStore(name = "settings") + +@Singleton +class SettingsDataStore @Inject constructor( + @ApplicationContext private val context: Context +) { + private val languageKey = stringPreferencesKey("language_code") + + val languageFlow: Flow = context.dataStore.data.map { preferences -> + preferences[languageKey] + } + + suspend fun setLanguage(languageCode: String?) { + context.dataStore.edit { preferences -> + if (languageCode == null) { + preferences.remove(languageKey) + } else { + preferences[languageKey] = languageCode + } + } + } + + fun getLanguageBlocking(): String? { + return runBlocking { + context.dataStore.data.first()[languageKey] + } + } +} diff --git a/app/src/main/java/com/fastmask/domain/model/Language.kt b/app/src/main/java/com/fastmask/domain/model/Language.kt new file mode 100644 index 0000000..e78b688 --- /dev/null +++ b/app/src/main/java/com/fastmask/domain/model/Language.kt @@ -0,0 +1,35 @@ +package com.fastmask.domain.model + +import com.fastmask.R + +enum class Language( + val code: String, + val displayNameRes: Int +) { + ENGLISH("en", R.string.language_en), + CHINESE("zh", R.string.language_zh), + SPANISH("es", R.string.language_es), + HINDI("hi", R.string.language_hi), + ARABIC("ar", R.string.language_ar), + PORTUGUESE("pt", R.string.language_pt), + BENGALI("bn", R.string.language_bn), + RUSSIAN("ru", R.string.language_ru), + JAPANESE("ja", R.string.language_ja), + FRENCH("fr", R.string.language_fr), + GERMAN("de", R.string.language_de), + KOREAN("ko", R.string.language_ko), + ITALIAN("it", R.string.language_it), + TURKISH("tr", R.string.language_tr), + VIETNAMESE("vi", R.string.language_vi), + POLISH("pl", R.string.language_pl), + UKRAINIAN("uk", R.string.language_uk), + DUTCH("nl", R.string.language_nl), + THAI("th", R.string.language_th), + INDONESIAN("id", R.string.language_id); + + companion object { + fun fromCode(code: String?): Language? { + return entries.find { it.code == code } + } + } +} diff --git a/app/src/main/java/com/fastmask/domain/usecase/GetCurrentLanguageUseCase.kt b/app/src/main/java/com/fastmask/domain/usecase/GetCurrentLanguageUseCase.kt new file mode 100644 index 0000000..1fc63ee --- /dev/null +++ b/app/src/main/java/com/fastmask/domain/usecase/GetCurrentLanguageUseCase.kt @@ -0,0 +1,21 @@ +package com.fastmask.domain.usecase + +import com.fastmask.data.local.SettingsDataStore +import com.fastmask.domain.model.Language +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class GetCurrentLanguageUseCase @Inject constructor( + private val settingsDataStore: SettingsDataStore +) { + operator fun invoke(): Flow { + return settingsDataStore.languageFlow.map { code -> + Language.fromCode(code) + } + } + + fun getBlocking(): Language? { + return Language.fromCode(settingsDataStore.getLanguageBlocking()) + } +} diff --git a/app/src/main/java/com/fastmask/domain/usecase/SetLanguageUseCase.kt b/app/src/main/java/com/fastmask/domain/usecase/SetLanguageUseCase.kt new file mode 100644 index 0000000..939826c --- /dev/null +++ b/app/src/main/java/com/fastmask/domain/usecase/SetLanguageUseCase.kt @@ -0,0 +1,23 @@ +package com.fastmask.domain.usecase + +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.LocaleListCompat +import com.fastmask.data.local.SettingsDataStore +import com.fastmask.domain.model.Language +import javax.inject.Inject + +class SetLanguageUseCase @Inject constructor( + private val settingsDataStore: SettingsDataStore +) { + suspend operator fun invoke(language: Language?) { + val languageCode = language?.code + settingsDataStore.setLanguage(languageCode) + + val localeList = if (languageCode != null) { + LocaleListCompat.forLanguageTags(languageCode) + } else { + LocaleListCompat.getEmptyLocaleList() + } + AppCompatDelegate.setApplicationLocales(localeList) + } +} diff --git a/app/src/main/java/com/fastmask/ui/auth/LoginScreen.kt b/app/src/main/java/com/fastmask/ui/auth/LoginScreen.kt index 159bf10..151ec04 100644 --- a/app/src/main/java/com/fastmask/ui/auth/LoginScreen.kt +++ b/app/src/main/java/com/fastmask/ui/auth/LoginScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -42,6 +43,7 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import com.fastmask.R import kotlinx.coroutines.flow.collectLatest @Composable @@ -80,13 +82,13 @@ fun LoginScreen( Spacer(modifier = Modifier.height(16.dp)) Text( - text = "FastMask", + text = stringResource(R.string.app_name), style = MaterialTheme.typography.headlineLarge, color = MaterialTheme.colorScheme.primary ) Text( - text = "Fastmail Masked Email Manager", + text = stringResource(R.string.login_subtitle), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -96,8 +98,8 @@ fun LoginScreen( OutlinedTextField( value = uiState.token, onValueChange = viewModel::onTokenChange, - label = { Text("API Token") }, - placeholder = { Text("Enter your Fastmail API token") }, + label = { Text(stringResource(R.string.login_api_token_label)) }, + placeholder = { Text(stringResource(R.string.login_api_token_placeholder)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), visualTransformation = if (showToken) VisualTransformation.None else PasswordVisualTransformation(), @@ -112,7 +114,11 @@ fun LoginScreen( IconButton(onClick = { showToken = !showToken }) { Icon( imageVector = if (showToken) Icons.Default.VisibilityOff else Icons.Default.Visibility, - contentDescription = if (showToken) "Hide token" else "Show token" + contentDescription = if (showToken) { + stringResource(R.string.login_hide_token) + } else { + stringResource(R.string.login_show_token) + } ) } }, @@ -143,7 +149,7 @@ fun LoginScreen( strokeWidth = 2.dp ) } else { - Text("Login") + Text(stringResource(R.string.login_button)) } } @@ -159,17 +165,13 @@ fun LoginScreen( modifier = Modifier.padding(16.dp) ) { Text( - text = "How to get your API token:", + text = stringResource(R.string.login_instructions_title), style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "1. Log in to Fastmail web app\n" + - "2. Go to Settings > Privacy & Security\n" + - "3. Click on Integrations > API tokens\n" + - "4. Create a new token with \"Masked Email\" scope\n" + - "5. Copy the token and paste it above", + text = stringResource(R.string.login_instructions_full), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Start diff --git a/app/src/main/java/com/fastmask/ui/components/ErrorMessage.kt b/app/src/main/java/com/fastmask/ui/components/ErrorMessage.kt index 621d701..49d462f 100644 --- a/app/src/main/java/com/fastmask/ui/components/ErrorMessage.kt +++ b/app/src/main/java/com/fastmask/ui/components/ErrorMessage.kt @@ -16,8 +16,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.fastmask.R @Composable fun ErrorMessage( @@ -48,7 +50,7 @@ fun ErrorMessage( if (onRetry != null) { Spacer(modifier = Modifier.height(24.dp)) Button(onClick = onRetry) { - Text("Retry") + Text(stringResource(R.string.error_retry)) } } } diff --git a/app/src/main/java/com/fastmask/ui/components/MaskedEmailCard.kt b/app/src/main/java/com/fastmask/ui/components/MaskedEmailCard.kt index 3bdeae5..763ccce 100644 --- a/app/src/main/java/com/fastmask/ui/components/MaskedEmailCard.kt +++ b/app/src/main/java/com/fastmask/ui/components/MaskedEmailCard.kt @@ -30,11 +30,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.fastmask.R import com.fastmask.domain.model.EmailState import com.fastmask.domain.model.MaskedEmail import com.fastmask.ui.theme.FastMaskStatusColors @@ -52,10 +53,10 @@ fun MaskedEmailCard( val haptic = LocalHapticFeedback.current val stateDescription = when (maskedEmail.state) { - EmailState.ENABLED -> "Enabled" - EmailState.DISABLED -> "Disabled" - EmailState.DELETED -> "Deleted" - EmailState.PENDING -> "Pending" + EmailState.ENABLED -> stringResource(R.string.state_enabled) + EmailState.DISABLED -> stringResource(R.string.state_disabled) + EmailState.DELETED -> stringResource(R.string.state_deleted) + EmailState.PENDING -> stringResource(R.string.state_pending) } with(sharedTransitionScope) { diff --git a/app/src/main/java/com/fastmask/ui/create/CreateMaskedEmailScreen.kt b/app/src/main/java/com/fastmask/ui/create/CreateMaskedEmailScreen.kt index 3674494..6d59fb5 100644 --- a/app/src/main/java/com/fastmask/ui/create/CreateMaskedEmailScreen.kt +++ b/app/src/main/java/com/fastmask/ui/create/CreateMaskedEmailScreen.kt @@ -41,10 +41,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import com.fastmask.R import com.fastmask.domain.model.EmailState import kotlinx.coroutines.flow.collectLatest @@ -59,13 +61,17 @@ fun CreateMaskedEmailScreen( val snackbarHostState = remember { SnackbarHostState() } val haptic = LocalHapticFeedback.current + val createdMessage = stringResource(R.string.create_email_created, "%s") + val copyAction = stringResource(R.string.create_email_copy_action) + val navigateBackDesc = stringResource(R.string.navigate_back) + LaunchedEffect(Unit) { viewModel.events.collectLatest { event -> when (event) { is CreateMaskedEmailEvent.Created -> { val result = snackbarHostState.showSnackbar( - message = "Created: ${event.email}", - actionLabel = "Copy", + message = createdMessage.replace("%s", event.email), + actionLabel = copyAction, duration = SnackbarDuration.Long ) if (result == SnackbarResult.ActionPerformed) { @@ -82,7 +88,7 @@ fun CreateMaskedEmailScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Create Masked Email") }, + title = { Text(stringResource(R.string.create_email_title)) }, navigationIcon = { IconButton( onClick = { @@ -90,7 +96,7 @@ fun CreateMaskedEmailScreen( onNavigateBack() }, modifier = Modifier.semantics { - contentDescription = "Navigate back" + contentDescription = navigateBackDesc } ) { Icon( @@ -119,12 +125,12 @@ fun CreateMaskedEmailScreen( OutlinedTextField( value = uiState.emailPrefix, onValueChange = viewModel::onPrefixChange, - label = { Text("Email Prefix (optional)") }, - placeholder = { Text("e.g., mysite_shopping") }, + label = { Text(stringResource(R.string.create_email_prefix_label)) }, + placeholder = { Text(stringResource(R.string.create_email_prefix_placeholder)) }, supportingText = { Text( text = uiState.prefixError - ?: "Max 64 chars: lowercase letters, numbers, underscores" + ?: stringResource(R.string.create_email_prefix_hint) ) }, isError = uiState.prefixError != null, @@ -138,8 +144,8 @@ fun CreateMaskedEmailScreen( OutlinedTextField( value = uiState.forDomain, onValueChange = viewModel::onDomainChange, - label = { Text("Associated Domain (optional)") }, - placeholder = { Text("e.g., example.com") }, + label = { Text(stringResource(R.string.create_email_domain_label)) }, + placeholder = { Text(stringResource(R.string.create_email_domain_placeholder)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), enabled = !uiState.isLoading @@ -150,8 +156,8 @@ fun CreateMaskedEmailScreen( OutlinedTextField( value = uiState.description, onValueChange = viewModel::onDescriptionChange, - label = { Text("Description (optional)") }, - placeholder = { Text("e.g., Shopping account") }, + label = { Text(stringResource(R.string.create_email_description_label)) }, + placeholder = { Text(stringResource(R.string.create_email_description_placeholder)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), enabled = !uiState.isLoading @@ -162,8 +168,8 @@ fun CreateMaskedEmailScreen( OutlinedTextField( value = uiState.url, onValueChange = viewModel::onUrlChange, - label = { Text("URL (optional)") }, - placeholder = { Text("e.g., https://example.com") }, + label = { Text(stringResource(R.string.create_email_url_label)) }, + placeholder = { Text(stringResource(R.string.create_email_url_placeholder)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), enabled = !uiState.isLoading @@ -172,7 +178,7 @@ fun CreateMaskedEmailScreen( Spacer(modifier = Modifier.height(24.dp)) Text( - text = "Initial State", + text = stringResource(R.string.create_email_initial_state), style = MaterialTheme.typography.titleMedium ) @@ -192,7 +198,11 @@ fun CreateMaskedEmailScreen( enabled = !uiState.isLoading ) Text( - text = state.name.lowercase().replaceFirstChar { it.uppercase() }, + text = when (state) { + EmailState.ENABLED -> stringResource(R.string.state_enabled) + EmailState.DISABLED -> stringResource(R.string.state_disabled) + else -> state.name.lowercase().replaceFirstChar { it.uppercase() } + }, style = MaterialTheme.typography.bodyLarge ) } @@ -224,7 +234,7 @@ fun CreateMaskedEmailScreen( strokeWidth = 2.dp ) } else { - Text("Create Masked Email") + Text(stringResource(R.string.create_email_button)) } } } diff --git a/app/src/main/java/com/fastmask/ui/detail/MaskedEmailDetailScreen.kt b/app/src/main/java/com/fastmask/ui/detail/MaskedEmailDetailScreen.kt index 5d4acf1..1af6672 100644 --- a/app/src/main/java/com/fastmask/ui/detail/MaskedEmailDetailScreen.kt +++ b/app/src/main/java/com/fastmask/ui/detail/MaskedEmailDetailScreen.kt @@ -48,7 +48,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar @@ -67,11 +66,13 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import com.fastmask.R import com.fastmask.domain.model.EmailState import com.fastmask.ui.components.ErrorMessage import com.fastmask.ui.components.LoadingIndicator @@ -94,18 +95,25 @@ fun MaskedEmailDetailScreen( val scope = rememberCoroutineScope() val haptic = LocalHapticFeedback.current + val updatedMessage = stringResource(R.string.email_detail_updated) + val deletedMessage = stringResource(R.string.email_detail_deleted) + val copiedMessage = stringResource(R.string.email_detail_copied) + val navigateBackDesc = stringResource(R.string.navigate_back) + val deleteEmailDesc = stringResource(R.string.email_detail_delete) + val copyEmailDesc = stringResource(R.string.email_detail_copy_email) + LaunchedEffect(Unit) { viewModel.events.collectLatest { event -> when (event) { is MaskedEmailDetailEvent.Updated -> { snackbarHostState.showSnackbar( - message = "Updated successfully", + message = updatedMessage, duration = SnackbarDuration.Short ) } is MaskedEmailDetailEvent.Deleted -> { snackbarHostState.showSnackbar( - message = "Deleted successfully", + message = deletedMessage, duration = SnackbarDuration.Short ) onNavigateBack() @@ -117,8 +125,8 @@ fun MaskedEmailDetailScreen( if (showDeleteDialog) { AlertDialog( onDismissRequest = { showDeleteDialog = false }, - title = { Text("Delete Masked Email") }, - text = { Text("Are you sure you want to delete this masked email? This action cannot be undone.") }, + title = { Text(stringResource(R.string.email_detail_delete_dialog_title)) }, + text = { Text(stringResource(R.string.email_detail_delete_dialog_message)) }, confirmButton = { TextButton( onClick = { @@ -129,12 +137,12 @@ fun MaskedEmailDetailScreen( contentColor = MaterialTheme.colorScheme.error ) ) { - Text("Delete") + Text(stringResource(R.string.email_detail_delete_confirm)) } }, dismissButton = { TextButton(onClick = { showDeleteDialog = false }) { - Text("Cancel") + Text(stringResource(R.string.email_detail_delete_cancel)) } } ) @@ -143,7 +151,7 @@ fun MaskedEmailDetailScreen( Scaffold( topBar = { TopAppBar( - title = { Text("Email Details") }, + title = { Text(stringResource(R.string.email_detail_title)) }, navigationIcon = { IconButton( onClick = { @@ -151,7 +159,7 @@ fun MaskedEmailDetailScreen( onNavigateBack() }, modifier = Modifier.semantics { - contentDescription = "Navigate back" + contentDescription = navigateBackDesc } ) { Icon( @@ -168,7 +176,7 @@ fun MaskedEmailDetailScreen( showDeleteDialog = true }, modifier = Modifier.semantics { - contentDescription = "Delete email" + contentDescription = deleteEmailDesc } ) { Icon( @@ -225,11 +233,12 @@ fun MaskedEmailDetailScreen( clipboard.setPrimaryClip(clip) scope.launch { snackbarHostState.showSnackbar( - message = "Copied to clipboard", + message = copiedMessage, duration = SnackbarDuration.Short ) } }, + copyEmailDesc = copyEmailDesc, sharedTransitionScope = sharedTransitionScope, animatedContentScope = animatedContentScope, modifier = Modifier.padding(paddingValues) @@ -250,6 +259,7 @@ private fun EmailDetailContent( onToggleState: () -> Unit, onSaveChanges: () -> Unit, onCopyEmail: (String) -> Unit, + copyEmailDesc: String, sharedTransitionScope: SharedTransitionScope, animatedContentScope: AnimatedContentScope, modifier: Modifier = Modifier @@ -323,7 +333,7 @@ private fun EmailDetailContent( onCopyEmail(email.email) }, modifier = Modifier.semantics { - contentDescription = "Copy email address" + contentDescription = copyEmailDesc } ) { Icon( @@ -339,14 +349,18 @@ private fun EmailDetailContent( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Status: ", + text = stringResource(R.string.email_detail_status), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = " ", style = MaterialTheme.typography.bodyMedium ) val statusText = when (email.state) { - EmailState.ENABLED -> "Enabled" - EmailState.DISABLED -> "Disabled" - EmailState.DELETED -> "Deleted" - EmailState.PENDING -> "Pending" + EmailState.ENABLED -> stringResource(R.string.state_enabled) + EmailState.DISABLED -> stringResource(R.string.state_disabled) + EmailState.DELETED -> stringResource(R.string.state_deleted) + EmailState.PENDING -> stringResource(R.string.state_pending) } Text( text = statusText, @@ -359,7 +373,7 @@ private fun EmailDetailContent( email.createdBy?.let { createdBy -> Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Created by: $createdBy", + text = stringResource(R.string.email_detail_created_by, createdBy), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -368,7 +382,7 @@ private fun EmailDetailContent( email.formattedCreatedAt?.let { createdAt -> Spacer(modifier = Modifier.height(4.dp)) Text( - text = "Created: $createdAt", + text = stringResource(R.string.email_detail_created, createdAt), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -377,7 +391,7 @@ private fun EmailDetailContent( email.formattedLastMessageAt?.let { lastMessage -> Spacer(modifier = Modifier.height(4.dp)) Text( - text = "Last message: $lastMessage", + text = stringResource(R.string.email_detail_last_message, lastMessage), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -410,7 +424,7 @@ private fun EmailDetailContent( strokeWidth = 2.dp ) } else { - Text("Enable") + Text(stringResource(R.string.email_detail_enable)) } } } @@ -429,7 +443,7 @@ private fun EmailDetailContent( strokeWidth = 2.dp ) } else { - Text("Disable") + Text(stringResource(R.string.email_detail_disable)) } } } @@ -438,7 +452,7 @@ private fun EmailDetailContent( Spacer(modifier = Modifier.height(24.dp)) Text( - text = "Edit Details", + text = stringResource(R.string.email_detail_edit_title), style = MaterialTheme.typography.titleMedium ) @@ -447,7 +461,7 @@ private fun EmailDetailContent( OutlinedTextField( value = uiState.editedDescription, onValueChange = onDescriptionChange, - label = { Text("Description") }, + label = { Text(stringResource(R.string.email_detail_description_label)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), enabled = !uiState.isUpdating @@ -458,7 +472,7 @@ private fun EmailDetailContent( OutlinedTextField( value = uiState.editedForDomain, onValueChange = onForDomainChange, - label = { Text("Associated Domain") }, + label = { Text(stringResource(R.string.email_detail_domain_label)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), enabled = !uiState.isUpdating @@ -469,7 +483,7 @@ private fun EmailDetailContent( OutlinedTextField( value = uiState.editedUrl, onValueChange = onUrlChange, - label = { Text("URL") }, + label = { Text(stringResource(R.string.email_detail_url_label)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), enabled = !uiState.isUpdating @@ -501,7 +515,7 @@ private fun EmailDetailContent( strokeWidth = 2.dp ) } else { - Text("Save Changes") + Text(stringResource(R.string.email_detail_save)) } } } diff --git a/app/src/main/java/com/fastmask/ui/list/MaskedEmailListScreen.kt b/app/src/main/java/com/fastmask/ui/list/MaskedEmailListScreen.kt index f684a3b..d656b68 100644 --- a/app/src/main/java/com/fastmask/ui/list/MaskedEmailListScreen.kt +++ b/app/src/main/java/com/fastmask/ui/list/MaskedEmailListScreen.kt @@ -21,8 +21,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.FilterList -import androidx.compose.material.icons.automirrored.filled.Logout import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -46,27 +46,30 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.repeatOnLifecycle import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.hilt.navigation.compose.hiltViewModel +import com.fastmask.R import com.fastmask.domain.model.MaskedEmail import com.fastmask.ui.components.ErrorMessage import com.fastmask.ui.components.MaskedEmailCard import com.fastmask.ui.components.ShimmerEmailList -import kotlinx.coroutines.flow.collectLatest @OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) @Composable fun MaskedEmailListScreen( onNavigateToCreate: () -> Unit, onNavigateToDetail: (String) -> Unit, - onLogout: () -> Unit, + onNavigateToSettings: () -> Unit, sharedTransitionScope: SharedTransitionScope, animatedContentScope: AnimatedContentScope, viewModel: MaskedEmailListViewModel = hiltViewModel() @@ -77,28 +80,32 @@ fun MaskedEmailListScreen( val listState = rememberLazyListState() val haptic = LocalHapticFeedback.current - val isScrolling by remember { - derivedStateOf { listState.isScrollInProgress } - } - val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 } } - LaunchedEffect(Unit) { - viewModel.events.collectLatest { event -> - when (event) { - is MaskedEmailListEvent.LoggedOut -> onLogout() - } + val isScrolling by remember { + derivedStateOf { listState.isScrollInProgress } + } + + val filterEmailsDesc = stringResource(R.string.email_list_filter_emails) + val settingsDesc = stringResource(R.string.email_list_settings) + val createDesc = stringResource(R.string.email_list_create_description) + + // Refresh the list when the screen resumes (e.g., after creating a new email) + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(lifecycleOwner) { + lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel.loadMaskedEmails() } } Scaffold( topBar = { TopAppBar( - title = { Text("Masked Emails") }, + title = { Text(stringResource(R.string.email_list_title)) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface, titleContentColor = MaterialTheme.colorScheme.onSurface @@ -111,7 +118,7 @@ fun MaskedEmailListScreen( showFilterMenu = true }, modifier = Modifier.semantics { - contentDescription = "Filter emails" + contentDescription = filterEmailsDesc } ) { Icon( @@ -125,7 +132,16 @@ fun MaskedEmailListScreen( ) { EmailFilter.entries.forEach { filter -> DropdownMenuItem( - text = { Text(filter.name.lowercase().replaceFirstChar { it.uppercase() }) }, + text = { + Text( + when (filter) { + EmailFilter.ALL -> stringResource(R.string.filter_all) + EmailFilter.ENABLED -> stringResource(R.string.filter_enabled) + EmailFilter.DISABLED -> stringResource(R.string.filter_disabled) + EmailFilter.DELETED -> stringResource(R.string.filter_deleted) + } + ) + }, onClick = { viewModel.onFilterChange(filter) showFilterMenu = false @@ -137,14 +153,14 @@ fun MaskedEmailListScreen( IconButton( onClick = { haptic.performHapticFeedback(HapticFeedbackType.LongPress) - viewModel.logout() + onNavigateToSettings() }, modifier = Modifier.semantics { - contentDescription = "Logout" + contentDescription = settingsDesc } ) { Icon( - imageVector = Icons.AutoMirrored.Filled.Logout, + imageVector = Icons.Default.Settings, contentDescription = null ) } @@ -164,11 +180,11 @@ fun MaskedEmailListScreen( contentDescription = null ) }, - text = { Text("Create") }, + text = { Text(stringResource(R.string.email_list_create_fab)) }, containerColor = MaterialTheme.colorScheme.tertiaryContainer, contentColor = MaterialTheme.colorScheme.onTertiaryContainer, modifier = Modifier.semantics { - contentDescription = "Create new masked email" + contentDescription = createDesc } ) } @@ -237,6 +253,7 @@ private fun M3SearchBar( modifier: Modifier = Modifier ) { val haptic = LocalHapticFeedback.current + val clearSearchDesc = stringResource(R.string.email_list_clear_search) SearchBar( inputField = { @@ -246,7 +263,7 @@ private fun M3SearchBar( onSearch = { onActiveChange(false) }, expanded = active, onExpandedChange = onActiveChange, - placeholder = { Text("Search emails...") }, + placeholder = { Text(stringResource(R.string.email_list_search_placeholder)) }, leadingIcon = { Icon( imageVector = Icons.Default.Search, @@ -263,7 +280,7 @@ private fun M3SearchBar( ) { Icon( imageVector = Icons.Default.Close, - contentDescription = "Clear search" + contentDescription = clearSearchDesc ) } } @@ -300,7 +317,16 @@ private fun FilterChips( haptic.performHapticFeedback(HapticFeedbackType.LongPress) onFilterSelected(filter) }, - label = { Text(filter.name.lowercase().replaceFirstChar { it.uppercase() }) } + label = { + Text( + when (filter) { + EmailFilter.ALL -> stringResource(R.string.filter_all) + EmailFilter.ENABLED -> stringResource(R.string.filter_enabled) + EmailFilter.DISABLED -> stringResource(R.string.filter_disabled) + EmailFilter.DELETED -> stringResource(R.string.filter_deleted) + } + ) + } ) } } @@ -329,7 +355,7 @@ private fun EmailList( contentAlignment = Alignment.Center ) { Text( - text = "No masked emails found", + text = stringResource(R.string.email_list_empty), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/app/src/main/java/com/fastmask/ui/navigation/FastMaskNavHost.kt b/app/src/main/java/com/fastmask/ui/navigation/FastMaskNavHost.kt index 7b82a72..bc57b3b 100644 --- a/app/src/main/java/com/fastmask/ui/navigation/FastMaskNavHost.kt +++ b/app/src/main/java/com/fastmask/ui/navigation/FastMaskNavHost.kt @@ -17,6 +17,7 @@ import com.fastmask.ui.auth.LoginScreen import com.fastmask.ui.create.CreateMaskedEmailScreen import com.fastmask.ui.detail.MaskedEmailDetailScreen import com.fastmask.ui.list.MaskedEmailListScreen +import com.fastmask.ui.settings.SettingsScreen private const val TRANSITION_DURATION_MS = 300 @@ -79,13 +80,24 @@ fun FastMaskNavHost( onNavigateToDetail = { emailId -> navController.navigate(NavRoutes.emailDetail(emailId)) }, + onNavigateToSettings = { + navController.navigate(NavRoutes.SETTINGS) + }, + sharedTransitionScope = this@SharedTransitionLayout, + animatedContentScope = this@composable + ) + } + + composable(NavRoutes.SETTINGS) { + SettingsScreen( + onNavigateBack = { + navController.popBackStack() + }, onLogout = { navController.navigate(NavRoutes.LOGIN) { popUpTo(0) { inclusive = true } } - }, - sharedTransitionScope = this@SharedTransitionLayout, - animatedContentScope = this@composable + } ) } diff --git a/app/src/main/java/com/fastmask/ui/navigation/NavRoutes.kt b/app/src/main/java/com/fastmask/ui/navigation/NavRoutes.kt index 74053ad..1bb7d41 100644 --- a/app/src/main/java/com/fastmask/ui/navigation/NavRoutes.kt +++ b/app/src/main/java/com/fastmask/ui/navigation/NavRoutes.kt @@ -5,6 +5,7 @@ object NavRoutes { const val EMAIL_LIST = "email_list" const val CREATE_EMAIL = "create_email" const val EMAIL_DETAIL = "email_detail/{emailId}" + const val SETTINGS = "settings" fun emailDetail(emailId: String) = "email_detail/$emailId" } diff --git a/app/src/main/java/com/fastmask/ui/settings/SettingsScreen.kt b/app/src/main/java/com/fastmask/ui/settings/SettingsScreen.kt new file mode 100644 index 0000000..71ddbe6 --- /dev/null +++ b/app/src/main/java/com/fastmask/ui/settings/SettingsScreen.kt @@ -0,0 +1,304 @@ +package com.fastmask.ui.settings + +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Logout +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.Email +import androidx.compose.material.icons.filled.Language +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.fastmask.BuildConfig +import com.fastmask.R +import com.fastmask.domain.model.Language +import kotlinx.coroutines.flow.collectLatest + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen( + onNavigateBack: () -> Unit, + onLogout: () -> Unit, + viewModel: SettingsViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsState() + val context = LocalContext.current + val haptic = LocalHapticFeedback.current + var showLanguageDialog by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + viewModel.events.collectLatest { event -> + when (event) { + is SettingsEvent.LoggedOut -> onLogout() + } + } + } + + if (showLanguageDialog) { + LanguagePickerDialog( + selectedLanguage = uiState.selectedLanguage, + onLanguageSelected = { language -> + viewModel.onLanguageSelected(language) + showLanguageDialog = false + }, + onDismiss = { showLanguageDialog = false } + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.settings_title)) }, + navigationIcon = { + IconButton( + onClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onNavigateBack() + }, + modifier = Modifier.semantics { + contentDescription = "Navigate back" + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface + ) + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + // Language Setting + ListItem( + headlineContent = { Text(stringResource(R.string.settings_language)) }, + supportingContent = { + val languageName = uiState.selectedLanguage?.let { + stringResource(it.displayNameRes) + } ?: stringResource(R.string.settings_system_default) + Text(languageName) + }, + leadingContent = { + Icon( + imageVector = Icons.Default.Language, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + }, + trailingContent = { + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = null + ) + }, + modifier = Modifier.clickable { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + showLanguageDialog = true + } + ) + + HorizontalDivider() + + // Contact & Feedback + ListItem( + headlineContent = { Text(stringResource(R.string.settings_contact)) }, + supportingContent = { Text(stringResource(R.string.settings_contact_description)) }, + leadingContent = { + Icon( + imageVector = Icons.Default.Email, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + }, + trailingContent = { + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = null + ) + }, + modifier = Modifier.clickable { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + val emailIntent = Intent(Intent.ACTION_SENDTO).apply { + data = Uri.parse("mailto:") + putExtra(Intent.EXTRA_EMAIL, arrayOf("pawel@orzech.me")) + putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.settings_feedback_subject)) + } + if (emailIntent.resolveActivity(context.packageManager) != null) { + context.startActivity(emailIntent) + } + } + ) + + HorizontalDivider() + + // Logout + ListItem( + headlineContent = { Text(stringResource(R.string.settings_logout)) }, + supportingContent = { Text(stringResource(R.string.settings_logout_description)) }, + leadingContent = { + Icon( + imageVector = Icons.AutoMirrored.Filled.Logout, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.error + ) + }, + modifier = Modifier.clickable { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + viewModel.logout() + } + ) + + Spacer(modifier = Modifier.weight(1f)) + + // Version + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.settings_version, BuildConfig.VERSION_NAME), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + +@Composable +private fun LanguagePickerDialog( + selectedLanguage: Language?, + onLanguageSelected: (Language?) -> Unit, + onDismiss: () -> Unit +) { + val haptic = LocalHapticFeedback.current + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.settings_select_language)) }, + text = { + LazyColumn( + modifier = Modifier.selectableGroup() + ) { + // System Default option + item { + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = selectedLanguage == null, + onClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onLanguageSelected(null) + }, + role = Role.RadioButton + ) + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedLanguage == null, + onClick = null + ) + Text( + text = stringResource(R.string.settings_system_default), + modifier = Modifier.padding(start = 16.dp), + style = MaterialTheme.typography.bodyLarge + ) + } + } + + items(Language.entries) { language -> + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = selectedLanguage == language, + onClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onLanguageSelected(language) + }, + role = Role.RadioButton + ) + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedLanguage == language, + onClick = null + ) + Text( + text = stringResource(language.displayNameRes), + modifier = Modifier.padding(start = 16.dp), + style = MaterialTheme.typography.bodyLarge + ) + } + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.email_detail_delete_cancel)) + } + } + ) +} diff --git a/app/src/main/java/com/fastmask/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/fastmask/ui/settings/SettingsViewModel.kt new file mode 100644 index 0000000..6f64b0b --- /dev/null +++ b/app/src/main/java/com/fastmask/ui/settings/SettingsViewModel.kt @@ -0,0 +1,66 @@ +package com.fastmask.ui.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.fastmask.domain.model.Language +import com.fastmask.domain.usecase.GetCurrentLanguageUseCase +import com.fastmask.domain.usecase.LogoutUseCase +import com.fastmask.domain.usecase.SetLanguageUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val getCurrentLanguageUseCase: GetCurrentLanguageUseCase, + private val setLanguageUseCase: SetLanguageUseCase, + private val logoutUseCase: LogoutUseCase +) : ViewModel() { + + private val _uiState = MutableStateFlow(SettingsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _events = MutableSharedFlow() + val events: SharedFlow = _events.asSharedFlow() + + init { + loadCurrentLanguage() + } + + private fun loadCurrentLanguage() { + viewModelScope.launch { + getCurrentLanguageUseCase().collect { language -> + _uiState.update { it.copy(selectedLanguage = language) } + } + } + } + + fun onLanguageSelected(language: Language?) { + viewModelScope.launch { + setLanguageUseCase(language) + _uiState.update { it.copy(selectedLanguage = language) } + } + } + + fun logout() { + logoutUseCase() + viewModelScope.launch { + _events.emit(SettingsEvent.LoggedOut) + } + } +} + +data class SettingsUiState( + val selectedLanguage: Language? = null +) + +sealed class SettingsEvent { + data object LoggedOut : SettingsEvent() +} diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000..3727319 --- /dev/null +++ b/app/src/main/res/values-ar/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + مدير البريد المقنّع من Fastmail + رمز API + أدخل رمز API الخاص بـ Fastmail + تسجيل الدخول + إظهار الرمز + إخفاء الرمز + كيفية الحصول على رمز API: + ١. سجّل الدخول إلى تطبيق Fastmail + ٢. انتقل إلى الإعدادات > الخصوصية والأمان + ٣. انقر على التكاملات > رموز API + ٤. أنشئ رمزاً جديداً بصلاحية \"Masked Email\" + ٥. انسخ الرمز والصقه أعلاه + ١. سجّل الدخول إلى تطبيق Fastmail\n٢. انتقل إلى الإعدادات > الخصوصية والأمان\n٣. انقر على التكاملات > رموز API\n٤. أنشئ رمزاً جديداً بصلاحية \"Masked Email\"\n٥. انسخ الرمز والصقه أعلاه + + + البريد المقنّع + البحث في البريد… + مسح البحث + تصفية البريد + إنشاء + إنشاء بريد مقنّع جديد + لم يتم العثور على بريد مقنّع + تسجيل الخروج + الإعدادات + الكل + مُفعّل + معطّل + محذوف + + + إنشاء بريد مقنّع + بادئة البريد (اختياري) + مثال: mysite_shopping + الحد الأقصى ٦٤ حرفاً: أحرف صغيرة، أرقام، شرطات سفلية + النطاق المرتبط (اختياري) + مثال: example.com + الوصف (اختياري) + مثال: حساب التسوق + الرابط (اختياري) + مثال: https://example.com + الحالة الأولية + إنشاء بريد مقنّع + تم الإنشاء: %s + نسخ + رجوع + + + تفاصيل البريد + نسخ عنوان البريد + حذف البريد + الحالة: + أنشأه: %s + تاريخ الإنشاء: %s + آخر رسالة: %s + تفعيل + تعطيل + تعديل التفاصيل + الوصف + النطاق المرتبط + الرابط + حفظ التغييرات + تم التحديث بنجاح + تم الحذف بنجاح + تم النسخ إلى الحافظة + حذف البريد المقنّع + هل أنت متأكد أنك تريد حذف هذا البريد المقنّع؟ لا يمكن التراجع عن هذا الإجراء. + حذف + إلغاء + + + مُفعّل + معطّل + محذوف + قيد الانتظار + + + فشل تحميل البريد + إعادة المحاولة + حدث خطأ + + + الإعدادات + اللغة + اختر لغة التطبيق + الافتراضي للنظام + التواصل والملاحظات + أرسل لنا ملاحظاتك + تسجيل الخروج + الخروج من حسابك + الإصدار %s + اختر اللغة + ملاحظات FastMask + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml new file mode 100644 index 0000000..5e215aa --- /dev/null +++ b/app/src/main/res/values-bn/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Fastmail মাস্কড ইমেইল ম্যানেজার + API টোকেন + আপনার Fastmail API টোকেন দিন + লগইন + টোকেন দেখান + টোকেন লুকান + কীভাবে আপনার API টোকেন পাবেন: + ১. Fastmail ওয়েব অ্যাপে লগইন করুন + ২. সেটিংস > প্রাইভেসি এবং সিকিউরিটিতে যান + ৩. ইন্টিগ্রেশন > API টোকেনে ক্লিক করুন + ৪. \"Masked Email\" স্কোপ সহ একটি নতুন টোকেন তৈরি করুন + ৫. টোকেন কপি করুন এবং উপরে পেস্ট করুন + ১. Fastmail ওয়েব অ্যাপে লগইন করুন\n২. সেটিংস > প্রাইভেসি এবং সিকিউরিটিতে যান\n৩. ইন্টিগ্রেশন > API টোকেনে ক্লিক করুন\n৪. \"Masked Email\" স্কোপ সহ একটি নতুন টোকেন তৈরি করুন\n৫. টোকেন কপি করুন এবং উপরে পেস্ট করুন + + + মাস্কড ইমেইল + ইমেইল খুঁজুন… + সার্চ মুছুন + ইমেইল ফিল্টার করুন + তৈরি করুন + নতুন মাস্কড ইমেইল তৈরি করুন + কোনো মাস্কড ইমেইল পাওয়া যায়নি + লগআউট + সেটিংস + সব + সক্রিয় + নিষ্ক্রিয় + মুছে ফেলা + + + মাস্কড ইমেইল তৈরি করুন + ইমেইল প্রিফিক্স (ঐচ্ছিক) + যেমন, mysite_shopping + সর্বোচ্চ ৬৪ অক্ষর: ছোট হাতের অক্ষর, সংখ্যা, আন্ডারস্কোর + সংশ্লিষ্ট ডোমেইন (ঐচ্ছিক) + যেমন, example.com + বিবরণ (ঐচ্ছিক) + যেমন, শপিং অ্যাকাউন্ট + URL (ঐচ্ছিক) + যেমন, https://example.com + প্রাথমিক অবস্থা + মাস্কড ইমেইল তৈরি করুন + তৈরি হয়েছে: %s + কপি + পিছনে যান + + + ইমেইল বিবরণ + ইমেইল ঠিকানা কপি করুন + ইমেইল মুছুন + অবস্থা: + তৈরি করেছেন: %s + তৈরি: %s + শেষ বার্তা: %s + সক্রিয় করুন + নিষ্ক্রিয় করুন + বিবরণ সম্পাদনা করুন + বিবরণ + সংশ্লিষ্ট ডোমেইন + URL + পরিবর্তন সংরক্ষণ করুন + সফলভাবে আপডেট হয়েছে + সফলভাবে মুছে ফেলা হয়েছে + ক্লিপবোর্ডে কপি হয়েছে + মাস্কড ইমেইল মুছুন + আপনি কি নিশ্চিত যে এই মাস্কড ইমেইলটি মুছতে চান? এই কাজটি পূর্বাবস্থায় ফেরানো যাবে না। + মুছুন + বাতিল + + + সক্রিয় + নিষ্ক্রিয় + মুছে ফেলা + অপেক্ষমাণ + + + ইমেইল লোড করতে ব্যর্থ + পুনরায় চেষ্টা করুন + একটি ত্রুটি ঘটেছে + + + সেটিংস + ভাষা + অ্যাপের ভাষা নির্বাচন করুন + সিস্টেম ডিফল্ট + যোগাযোগ এবং প্রতিক্রিয়া + আমাদের আপনার প্রতিক্রিয়া পাঠান + লগআউট + আপনার অ্যাকাউন্ট থেকে সাইন আউট করুন + সংস্করণ %s + ভাষা নির্বাচন করুন + FastMask প্রতিক্রিয়া + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 0000000..7c5a864 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Fastmail Maskierte-E-Mail-Manager + API-Token + Geben Sie Ihr Fastmail API-Token ein + Anmelden + Token anzeigen + Token ausblenden + So erhalten Sie Ihr API-Token: + 1. Melden Sie sich bei der Fastmail Web-App an + 2. Gehen Sie zu Einstellungen > Datenschutz und Sicherheit + 3. Klicken Sie auf Integrationen > API-Token + 4. Erstellen Sie ein neues Token mit dem Bereich \"Masked Email\" + 5. Kopieren Sie das Token und fügen Sie es oben ein + 1. Melden Sie sich bei der Fastmail Web-App an\n2. Gehen Sie zu Einstellungen > Datenschutz und Sicherheit\n3. Klicken Sie auf Integrationen > API-Token\n4. Erstellen Sie ein neues Token mit dem Bereich \"Masked Email\"\n5. Kopieren Sie das Token und fügen Sie es oben ein + + + Maskierte E-Mails + E-Mails suchen… + Suche löschen + E-Mails filtern + Erstellen + Neue maskierte E-Mail erstellen + Keine maskierten E-Mails gefunden + Abmelden + Einstellungen + Alle + Aktiviert + Deaktiviert + Gelöscht + + + Maskierte E-Mail erstellen + E-Mail-Präfix (optional) + z.B. meinseite_einkauf + Max. 64 Zeichen: Kleinbuchstaben, Zahlen, Unterstriche + Zugehörige Domain (optional) + z.B. beispiel.de + Beschreibung (optional) + z.B. Einkaufskonto + URL (optional) + z.B. https://beispiel.de + Anfangszustand + Maskierte E-Mail erstellen + Erstellt: %s + Kopieren + Zurück + + + E-Mail-Details + E-Mail-Adresse kopieren + E-Mail löschen + Status: + Erstellt von: %s + Erstellt: %s + Letzte Nachricht: %s + Aktivieren + Deaktivieren + Details bearbeiten + Beschreibung + Zugehörige Domain + URL + Änderungen speichern + Erfolgreich aktualisiert + Erfolgreich gelöscht + In Zwischenablage kopiert + Maskierte E-Mail löschen + Sind Sie sicher, dass Sie diese maskierte E-Mail löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden. + Löschen + Abbrechen + + + Aktiviert + Deaktiviert + Gelöscht + Ausstehend + + + E-Mails konnten nicht geladen werden + Erneut versuchen + Ein Fehler ist aufgetreten + + + Einstellungen + Sprache + App-Sprache auswählen + Systemstandard + Kontakt und Feedback + Senden Sie uns Ihr Feedback + Abmelden + Von Ihrem Konto abmelden + Version %s + Sprache auswählen + FastMask Feedback + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 0000000..3f519f1 --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Gestor de correos enmascarados de Fastmail + Token de API + Introduce tu token de API de Fastmail + Iniciar sesión + Mostrar token + Ocultar token + Cómo obtener tu token de API: + 1. Inicia sesión en la aplicación web de Fastmail + 2. Ve a Configuración > Privacidad y Seguridad + 3. Haz clic en Integraciones > Tokens de API + 4. Crea un nuevo token con el alcance \"Masked Email\" + 5. Copia el token y pégalo arriba + 1. Inicia sesión en la aplicación web de Fastmail\n2. Ve a Configuración > Privacidad y Seguridad\n3. Haz clic en Integraciones > Tokens de API\n4. Crea un nuevo token con el alcance \"Masked Email\"\n5. Copia el token y pégalo arriba + + + Correos enmascarados + Buscar correos… + Borrar búsqueda + Filtrar correos + Crear + Crear nuevo correo enmascarado + No se encontraron correos enmascarados + Cerrar sesión + Configuración + Todos + Habilitados + Deshabilitados + Eliminados + + + Crear correo enmascarado + Prefijo de correo (opcional) + ej., misitio_compras + Máx 64 caracteres: letras minúsculas, números, guiones bajos + Dominio asociado (opcional) + ej., ejemplo.com + Descripción (opcional) + ej., Cuenta de compras + URL (opcional) + ej., https://ejemplo.com + Estado inicial + Crear correo enmascarado + Creado: %s + Copiar + Volver + + + Detalles del correo + Copiar dirección de correo + Eliminar correo + Estado: + Creado por: %s + Creado: %s + Último mensaje: %s + Habilitar + Deshabilitar + Editar detalles + Descripción + Dominio asociado + URL + Guardar cambios + Actualizado correctamente + Eliminado correctamente + Copiado al portapapeles + Eliminar correo enmascarado + ¿Estás seguro de que quieres eliminar este correo enmascarado? Esta acción no se puede deshacer. + Eliminar + Cancelar + + + Habilitado + Deshabilitado + Eliminado + Pendiente + + + Error al cargar los correos + Reintentar + Se produjo un error + + + Configuración + Idioma + Seleccionar idioma de la app + Predeterminado del sistema + Contacto y comentarios + Envíanos tus comentarios + Cerrar sesión + Salir de tu cuenta + Versión %s + Seleccionar idioma + Comentarios de FastMask + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..edefd41 --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Gestionnaire d\'emails masqués Fastmail + Jeton API + Entrez votre jeton API Fastmail + Connexion + Afficher le jeton + Masquer le jeton + Comment obtenir votre jeton API : + 1. Connectez-vous à l\'application web Fastmail + 2. Allez dans Paramètres > Confidentialité et sécurité + 3. Cliquez sur Intégrations > Jetons API + 4. Créez un nouveau jeton avec la portée \"Masked Email\" + 5. Copiez le jeton et collez-le ci-dessus + 1. Connectez-vous à l\'application web Fastmail\n2. Allez dans Paramètres > Confidentialité et sécurité\n3. Cliquez sur Intégrations > Jetons API\n4. Créez un nouveau jeton avec la portée \"Masked Email\"\n5. Copiez le jeton et collez-le ci-dessus + + + Emails masqués + Rechercher des emails… + Effacer la recherche + Filtrer les emails + Créer + Créer un nouvel email masqué + Aucun email masqué trouvé + Déconnexion + Paramètres + Tous + Activés + Désactivés + Supprimés + + + Créer un email masqué + Préfixe d\'email (optionnel) + ex. : monsite_achats + Max 64 caractères : lettres minuscules, chiffres, tirets bas + Domaine associé (optionnel) + ex. : exemple.com + Description (optionnel) + ex. : Compte d\'achats + URL (optionnel) + ex. : https://exemple.com + État initial + Créer l\'email masqué + Créé : %s + Copier + Retour + + + Détails de l\'email + Copier l\'adresse email + Supprimer l\'email + Statut : + Créé par : %s + Créé : %s + Dernier message : %s + Activer + Désactiver + Modifier les détails + Description + Domaine associé + URL + Enregistrer les modifications + Mis à jour avec succès + Supprimé avec succès + Copié dans le presse-papiers + Supprimer l\'email masqué + Êtes-vous sûr de vouloir supprimer cet email masqué ? Cette action est irréversible. + Supprimer + Annuler + + + Activé + Désactivé + Supprimé + En attente + + + Échec du chargement des emails + Réessayer + Une erreur s\'est produite + + + Paramètres + Langue + Sélectionner la langue de l\'app + Par défaut du système + Contact et commentaires + Envoyez-nous vos commentaires + Déconnexion + Se déconnecter de votre compte + Version %s + Sélectionner la langue + Commentaires FastMask + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml new file mode 100644 index 0000000..e9a3f3a --- /dev/null +++ b/app/src/main/res/values-hi/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Fastmail मास्क्ड ईमेल प्रबंधक + API टोकन + अपना Fastmail API टोकन दर्ज करें + लॉग इन करें + टोकन दिखाएं + टोकन छुपाएं + अपना API टोकन कैसे प्राप्त करें: + 1. Fastmail वेब ऐप में लॉग इन करें + 2. सेटिंग्स > गोपनीयता और सुरक्षा पर जाएं + 3. इंटीग्रेशन > API टोकन पर क्लिक करें + 4. \"Masked Email\" स्कोप के साथ एक नया टोकन बनाएं + 5. टोकन कॉपी करें और ऊपर पेस्ट करें + 1. Fastmail वेब ऐप में लॉग इन करें\n2. सेटिंग्स > गोपनीयता और सुरक्षा पर जाएं\n3. इंटीग्रेशन > API टोकन पर क्लिक करें\n4. \"Masked Email\" स्कोप के साथ एक नया टोकन बनाएं\n5. टोकन कॉपी करें और ऊपर पेस्ट करें + + + मास्क्ड ईमेल + ईमेल खोजें… + खोज साफ़ करें + ईमेल फ़िल्टर करें + बनाएं + नया मास्क्ड ईमेल बनाएं + कोई मास्क्ड ईमेल नहीं मिला + लॉग आउट + सेटिंग्स + सभी + सक्षम + अक्षम + हटाए गए + + + मास्क्ड ईमेल बनाएं + ईमेल प्रीफ़िक्स (वैकल्पिक) + जैसे, mysite_shopping + अधिकतम 64 अक्षर: छोटे अक्षर, संख्याएं, अंडरस्कोर + संबंधित डोमेन (वैकल्पिक) + जैसे, example.com + विवरण (वैकल्पिक) + जैसे, शॉपिंग अकाउंट + URL (वैकल्पिक) + जैसे, https://example.com + प्रारंभिक स्थिति + मास्क्ड ईमेल बनाएं + बनाया गया: %s + कॉपी करें + वापस जाएं + + + ईमेल विवरण + ईमेल पता कॉपी करें + ईमेल हटाएं + स्थिति: + बनाने वाला: %s + बनाया गया: %s + अंतिम संदेश: %s + सक्षम करें + अक्षम करें + विवरण संपादित करें + विवरण + संबंधित डोमेन + URL + परिवर्तन सहेजें + सफलतापूर्वक अपडेट किया गया + सफलतापूर्वक हटाया गया + क्लिपबोर्ड पर कॉपी किया गया + मास्क्ड ईमेल हटाएं + क्या आप वाकई इस मास्क्ड ईमेल को हटाना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती। + हटाएं + रद्द करें + + + सक्षम + अक्षम + हटाया गया + लंबित + + + ईमेल लोड करने में विफल + पुनः प्रयास करें + एक त्रुटि हुई + + + सेटिंग्स + भाषा + ऐप की भाषा चुनें + सिस्टम डिफ़ॉल्ट + संपर्क और फ़ीडबैक + हमें अपना फ़ीडबैक भेजें + लॉग आउट + अपने खाते से साइन आउट करें + संस्करण %s + भाषा चुनें + FastMask फ़ीडबैक + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml new file mode 100644 index 0000000..828414a --- /dev/null +++ b/app/src/main/res/values-id/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Pengelola Email Tersamar Fastmail + Token API + Masukkan token API Fastmail Anda + Masuk + Tampilkan token + Sembunyikan token + Cara mendapatkan token API: + 1. Masuk ke aplikasi web Fastmail + 2. Buka Pengaturan > Privasi & Keamanan + 3. Klik Integrasi > Token API + 4. Buat token baru dengan cakupan \"Masked Email\" + 5. Salin token dan tempel di atas + 1. Masuk ke aplikasi web Fastmail\n2. Buka Pengaturan > Privasi & Keamanan\n3. Klik Integrasi > Token API\n4. Buat token baru dengan cakupan \"Masked Email\"\n5. Salin token dan tempel di atas + + + Email Tersamar + Cari email… + Hapus pencarian + Filter email + Buat + Buat email tersamar baru + Tidak ada email tersamar ditemukan + Keluar + Pengaturan + Semua + Aktif + Nonaktif + Dihapus + + + Buat Email Tersamar + Awalan email (opsional) + mis., situsku_belanja + Maks 64 karakter: huruf kecil, angka, garis bawah + Domain terkait (opsional) + mis., contoh.com + Deskripsi (opsional) + mis., Akun belanja + URL (opsional) + mis., https://contoh.com + Status awal + Buat Email Tersamar + Dibuat: %s + Salin + Kembali + + + Detail Email + Salin alamat email + Hapus email + Status: + Dibuat oleh: %s + Dibuat: %s + Pesan terakhir: %s + Aktifkan + Nonaktifkan + Edit Detail + Deskripsi + Domain terkait + URL + Simpan Perubahan + Berhasil diperbarui + Berhasil dihapus + Disalin ke papan klip + Hapus Email Tersamar + Apakah Anda yakin ingin menghapus email tersamar ini? Tindakan ini tidak dapat dibatalkan. + Hapus + Batal + + + Aktif + Nonaktif + Dihapus + Tertunda + + + Gagal memuat email + Coba Lagi + Terjadi kesalahan + + + Pengaturan + Bahasa + Pilih bahasa aplikasi + Default Sistem + Kontak & Masukan + Kirimkan masukan Anda + Keluar + Keluar dari akun Anda + Versi %s + Pilih Bahasa + Masukan FastMask + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 0000000..f25d5af --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Gestore email mascherate di Fastmail + Token API + Inserisci il tuo token API Fastmail + Accedi + Mostra token + Nascondi token + Come ottenere il tuo token API: + 1. Accedi all\'app web di Fastmail + 2. Vai su Impostazioni > Privacy e Sicurezza + 3. Clicca su Integrazioni > Token API + 4. Crea un nuovo token con ambito \"Masked Email\" + 5. Copia il token e incollalo sopra + 1. Accedi all\'app web di Fastmail\n2. Vai su Impostazioni > Privacy e Sicurezza\n3. Clicca su Integrazioni > Token API\n4. Crea un nuovo token con ambito \"Masked Email\"\n5. Copia il token e incollalo sopra + + + Email mascherate + Cerca email… + Cancella ricerca + Filtra email + Crea + Crea nuova email mascherata + Nessuna email mascherata trovata + Esci + Impostazioni + Tutte + Attive + Disattivate + Eliminate + + + Crea email mascherata + Prefisso email (opzionale) + es. miosito_acquisti + Max 64 caratteri: lettere minuscole, numeri, trattini bassi + Dominio associato (opzionale) + es. esempio.it + Descrizione (opzionale) + es. Account acquisti + URL (opzionale) + es. https://esempio.it + Stato iniziale + Crea email mascherata + Creata: %s + Copia + Indietro + + + Dettagli email + Copia indirizzo email + Elimina email + Stato: + Creata da: %s + Creata: %s + Ultimo messaggio: %s + Attiva + Disattiva + Modifica dettagli + Descrizione + Dominio associato + URL + Salva modifiche + Aggiornato con successo + Eliminato con successo + Copiato negli appunti + Elimina email mascherata + Sei sicuro di voler eliminare questa email mascherata? Questa azione non può essere annullata. + Elimina + Annulla + + + Attiva + Disattivata + Eliminata + In attesa + + + Impossibile caricare le email + Riprova + Si è verificato un errore + + + Impostazioni + Lingua + Seleziona la lingua dell\'app + Predefinito di sistema + Contatto e feedback + Inviaci il tuo feedback + Esci + Disconnettiti dal tuo account + Versione %s + Seleziona lingua + Feedback FastMask + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000..8a67e8f --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Fastmail マスクメール管理 + APIトークン + Fastmail APIトークンを入力 + ログイン + トークンを表示 + トークンを非表示 + APIトークンの取得方法: + 1. Fastmail Webアプリにログイン + 2. 設定 > プライバシーとセキュリティに移動 + 3. 連携 > APIトークンをクリック + 4. 「Masked Email」スコープで新しいトークンを作成 + 5. トークンをコピーして上に貼り付け + 1. Fastmail Webアプリにログイン\n2. 設定 > プライバシーとセキュリティに移動\n3. 連携 > APIトークンをクリック\n4. 「Masked Email」スコープで新しいトークンを作成\n5. トークンをコピーして上に貼り付け + + + マスクメール + メールを検索… + 検索をクリア + メールをフィルター + 作成 + 新しいマスクメールを作成 + マスクメールが見つかりません + ログアウト + 設定 + すべて + 有効 + 無効 + 削除済み + + + マスクメールを作成 + メールプレフィックス(任意) + 例:mysite_shopping + 最大64文字:小文字、数字、アンダースコア + 関連ドメイン(任意) + 例:example.com + 説明(任意) + 例:ショッピングアカウント + URL(任意) + 例:https://example.com + 初期状態 + マスクメールを作成 + 作成完了:%s + コピー + 戻る + + + メール詳細 + メールアドレスをコピー + メールを削除 + 状態: + 作成者:%s + 作成日:%s + 最後のメッセージ:%s + 有効にする + 無効にする + 詳細を編集 + 説明 + 関連ドメイン + URL + 変更を保存 + 更新しました + 削除しました + クリップボードにコピーしました + マスクメールを削除 + このマスクメールを削除してもよろしいですか?この操作は元に戻せません。 + 削除 + キャンセル + + + 有効 + 無効 + 削除済み + 保留中 + + + メールの読み込みに失敗しました + 再試行 + エラーが発生しました + + + 設定 + 言語 + アプリの言語を選択 + システムの既定 + お問い合わせとフィードバック + フィードバックを送信 + ログアウト + アカウントからサインアウト + バージョン %s + 言語を選択 + FastMask フィードバック + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml new file mode 100644 index 0000000..875f768 --- /dev/null +++ b/app/src/main/res/values-ko/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Fastmail 마스크 이메일 관리자 + API 토큰 + Fastmail API 토큰을 입력하세요 + 로그인 + 토큰 표시 + 토큰 숨기기 + API 토큰 얻는 방법: + 1. Fastmail 웹 앱에 로그인 + 2. 설정 > 개인정보 및 보안으로 이동 + 3. 통합 > API 토큰 클릭 + 4. \"Masked Email\" 범위로 새 토큰 생성 + 5. 토큰을 복사하여 위에 붙여넣기 + 1. Fastmail 웹 앱에 로그인\n2. 설정 > 개인정보 및 보안으로 이동\n3. 통합 > API 토큰 클릭\n4. \"Masked Email\" 범위로 새 토큰 생성\n5. 토큰을 복사하여 위에 붙여넣기 + + + 마스크 이메일 + 이메일 검색… + 검색 지우기 + 이메일 필터 + 생성 + 새 마스크 이메일 생성 + 마스크 이메일을 찾을 수 없습니다 + 로그아웃 + 설정 + 전체 + 활성화됨 + 비활성화됨 + 삭제됨 + + + 마스크 이메일 생성 + 이메일 접두사 (선택) + 예: mysite_shopping + 최대 64자: 소문자, 숫자, 밑줄 + 연결된 도메인 (선택) + 예: example.com + 설명 (선택) + 예: 쇼핑 계정 + URL (선택) + 예: https://example.com + 초기 상태 + 마스크 이메일 생성 + 생성됨: %s + 복사 + 뒤로 + + + 이메일 상세 + 이메일 주소 복사 + 이메일 삭제 + 상태: + 생성자: %s + 생성일: %s + 마지막 메시지: %s + 활성화 + 비활성화 + 상세 정보 편집 + 설명 + 연결된 도메인 + URL + 변경 사항 저장 + 업데이트 완료 + 삭제 완료 + 클립보드에 복사됨 + 마스크 이메일 삭제 + 이 마스크 이메일을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. + 삭제 + 취소 + + + 활성화됨 + 비활성화됨 + 삭제됨 + 대기 중 + + + 이메일 로드 실패 + 다시 시도 + 오류가 발생했습니다 + + + 설정 + 언어 + 앱 언어 선택 + 시스템 기본값 + 연락처 및 피드백 + 피드백을 보내주세요 + 로그아웃 + 계정에서 로그아웃 + 버전 %s + 언어 선택 + FastMask 피드백 + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000..f36ab95 --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Fastmail Gemaskeerde E-mail Beheerder + API-token + Voer uw Fastmail API-token in + Inloggen + Token tonen + Token verbergen + Hoe u uw API-token krijgt: + 1. Log in op de Fastmail web-app + 2. Ga naar Instellingen > Privacy en Beveiliging + 3. Klik op Integraties > API-tokens + 4. Maak een nieuw token met \"Masked Email\" scope + 5. Kopieer het token en plak het hierboven + 1. Log in op de Fastmail web-app\n2. Ga naar Instellingen > Privacy en Beveiliging\n3. Klik op Integraties > API-tokens\n4. Maak een nieuw token met \"Masked Email\" scope\n5. Kopieer het token en plak het hierboven + + + Gemaskeerde e-mails + E-mails zoeken… + Zoekopdracht wissen + E-mails filteren + Aanmaken + Nieuwe gemaskeerde e-mail aanmaken + Geen gemaskeerde e-mails gevonden + Uitloggen + Instellingen + Alle + Ingeschakeld + Uitgeschakeld + Verwijderd + + + Gemaskeerde e-mail aanmaken + E-mail prefix (optioneel) + bijv. mijnsite_winkelen + Max 64 tekens: kleine letters, cijfers, underscores + Gekoppeld domein (optioneel) + bijv. voorbeeld.nl + Beschrijving (optioneel) + bijv. Winkelaccount + URL (optioneel) + bijv. https://voorbeeld.nl + Beginstatus + Gemaskeerde e-mail aanmaken + Aangemaakt: %s + Kopiëren + Terug + + + E-maildetails + E-mailadres kopiëren + E-mail verwijderen + Status: + Aangemaakt door: %s + Aangemaakt: %s + Laatste bericht: %s + Inschakelen + Uitschakelen + Details bewerken + Beschrijving + Gekoppeld domein + URL + Wijzigingen opslaan + Succesvol bijgewerkt + Succesvol verwijderd + Gekopieerd naar klembord + Gemaskeerde e-mail verwijderen + Weet u zeker dat u deze gemaskeerde e-mail wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt. + Verwijderen + Annuleren + + + Ingeschakeld + Uitgeschakeld + Verwijderd + In afwachting + + + Kon e-mails niet laden + Opnieuw proberen + Er is een fout opgetreden + + + Instellingen + Taal + App-taal selecteren + Systeemstandaard + Contact en feedback + Stuur ons uw feedback + Uitloggen + Afmelden bij uw account + Versie %s + Taal selecteren + FastMask Feedback + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000..a29b5e7 --- /dev/null +++ b/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Menedżer ukrytych adresów Fastmail + Token API + Wprowadź token API Fastmail + Zaloguj się + Pokaż token + Ukryj token + Jak uzyskać token API: + 1. Zaloguj się do aplikacji webowej Fastmail + 2. Przejdź do Ustawienia > Prywatność i bezpieczeństwo + 3. Kliknij Integracje > Tokeny API + 4. Utwórz nowy token z zakresem \"Masked Email\" + 5. Skopiuj token i wklej go powyżej + 1. Zaloguj się do aplikacji webowej Fastmail\n2. Przejdź do Ustawienia > Prywatność i bezpieczeństwo\n3. Kliknij Integracje > Tokeny API\n4. Utwórz nowy token z zakresem \"Masked Email\"\n5. Skopiuj token i wklej go powyżej + + + Ukryte adresy + Szukaj adresów… + Wyczyść wyszukiwanie + Filtruj adresy + Utwórz + Utwórz nowy ukryty adres + Nie znaleziono ukrytych adresów + Wyloguj + Ustawienia + Wszystkie + Aktywne + Wyłączone + Usunięte + + + Utwórz ukryty adres + Prefiks adresu (opcjonalnie) + np. mojastrona_zakupy + Maks. 64 znaki: małe litery, cyfry, podkreślenia + Powiązana domena (opcjonalnie) + np. przyklad.pl + Opis (opcjonalnie) + np. Konto zakupowe + URL (opcjonalnie) + np. https://przyklad.pl + Stan początkowy + Utwórz ukryty adres + Utworzono: %s + Kopiuj + Wstecz + + + Szczegóły adresu + Kopiuj adres e-mail + Usuń adres + Status: + Utworzony przez: %s + Utworzono: %s + Ostatnia wiadomość: %s + Włącz + Wyłącz + Edytuj szczegóły + Opis + Powiązana domena + URL + Zapisz zmiany + Zaktualizowano pomyślnie + Usunięto pomyślnie + Skopiowano do schowka + Usuń ukryty adres + Czy na pewno chcesz usunąć ten ukryty adres? Tej operacji nie można cofnąć. + Usuń + Anuluj + + + Aktywny + Wyłączony + Usunięty + Oczekujący + + + Nie udało się załadować adresów + Ponów próbę + Wystąpił błąd + + + Ustawienia + Język + Wybierz język aplikacji + Domyślny systemowy + Kontakt i opinie + Wyślij nam swoją opinię + Wyloguj + Wyloguj się z konta + Wersja %s + Wybierz język + Opinia o FastMask + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml new file mode 100644 index 0000000..4f4f58d --- /dev/null +++ b/app/src/main/res/values-pt/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Gerenciador de emails mascarados do Fastmail + Token de API + Digite seu token de API do Fastmail + Entrar + Mostrar token + Ocultar token + Como obter seu token de API: + 1. Faça login no aplicativo web do Fastmail + 2. Vá para Configurações > Privacidade e Segurança + 3. Clique em Integrações > Tokens de API + 4. Crie um novo token com o escopo \"Masked Email\" + 5. Copie o token e cole acima + 1. Faça login no aplicativo web do Fastmail\n2. Vá para Configurações > Privacidade e Segurança\n3. Clique em Integrações > Tokens de API\n4. Crie um novo token com o escopo \"Masked Email\"\n5. Copie o token e cole acima + + + Emails mascarados + Pesquisar emails… + Limpar pesquisa + Filtrar emails + Criar + Criar novo email mascarado + Nenhum email mascarado encontrado + Sair + Configurações + Todos + Ativados + Desativados + Excluídos + + + Criar email mascarado + Prefixo do email (opcional) + ex.: meusite_compras + Máx. 64 caracteres: letras minúsculas, números, sublinhados + Domínio associado (opcional) + ex.: exemplo.com + Descrição (opcional) + ex.: Conta de compras + URL (opcional) + ex.: https://exemplo.com + Estado inicial + Criar email mascarado + Criado: %s + Copiar + Voltar + + + Detalhes do email + Copiar endereço de email + Excluir email + Status: + Criado por: %s + Criado: %s + Última mensagem: %s + Ativar + Desativar + Editar detalhes + Descrição + Domínio associado + URL + Salvar alterações + Atualizado com sucesso + Excluído com sucesso + Copiado para a área de transferência + Excluir email mascarado + Tem certeza de que deseja excluir este email mascarado? Esta ação não pode ser desfeita. + Excluir + Cancelar + + + Ativado + Desativado + Excluído + Pendente + + + Falha ao carregar emails + Tentar novamente + Ocorreu um erro + + + Configurações + Idioma + Selecionar idioma do app + Padrão do sistema + Contato e feedback + Envie-nos seu feedback + Sair + Sair da sua conta + Versão %s + Selecionar idioma + Feedback do FastMask + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000..e842d85 --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Менеджер масок email для Fastmail + API токен + Введите ваш API токен Fastmail + Войти + Показать токен + Скрыть токен + Как получить API токен: + 1. Войдите в веб-приложение Fastmail + 2. Перейдите в Настройки > Конфиденциальность и безопасность + 3. Нажмите Интеграции > API токены + 4. Создайте новый токен с правами \"Masked Email\" + 5. Скопируйте токен и вставьте выше + 1. Войдите в веб-приложение Fastmail\n2. Перейдите в Настройки > Конфиденциальность и безопасность\n3. Нажмите Интеграции > API токены\n4. Создайте новый токен с правами \"Masked Email\"\n5. Скопируйте токен и вставьте выше + + + Маски email + Поиск email… + Очистить поиск + Фильтровать email + Создать + Создать новую маску email + Маски email не найдены + Выйти + Настройки + Все + Активные + Отключённые + Удалённые + + + Создать маску email + Префикс email (необязательно) + напр., mysite_shopping + Макс. 64 символа: строчные буквы, цифры, подчёркивания + Связанный домен (необязательно) + напр., example.com + Описание (необязательно) + напр., Аккаунт для покупок + URL (необязательно) + напр., https://example.com + Начальное состояние + Создать маску email + Создано: %s + Копировать + Назад + + + Детали email + Копировать адрес email + Удалить email + Статус: + Создано: %s + Создано: %s + Последнее сообщение: %s + Включить + Отключить + Редактировать + Описание + Связанный домен + URL + Сохранить изменения + Успешно обновлено + Успешно удалено + Скопировано в буфер обмена + Удалить маску email + Вы уверены, что хотите удалить эту маску email? Это действие нельзя отменить. + Удалить + Отмена + + + Активна + Отключена + Удалена + Ожидание + + + Не удалось загрузить email + Повторить + Произошла ошибка + + + Настройки + Язык + Выбрать язык приложения + Системный по умолчанию + Связаться и оставить отзыв + Отправьте нам отзыв + Выйти + Выйти из аккаунта + Версия %s + Выбрать язык + Отзыв о FastMask + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml new file mode 100644 index 0000000..178148d --- /dev/null +++ b/app/src/main/res/values-th/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + ตัวจัดการอีเมลซ่อน Fastmail + โทเค็น API + ใส่โทเค็น API ของ Fastmail + เข้าสู่ระบบ + แสดงโทเค็น + ซ่อนโทเค็น + วิธีรับโทเค็น API: + 1. เข้าสู่ระบบแอป Fastmail บนเว็บ + 2. ไปที่ ตั้งค่า > ความเป็นส่วนตัวและความปลอดภัย + 3. คลิก การเชื่อมต่อ > โทเค็น API + 4. สร้างโทเค็นใหม่ที่มีขอบเขต \"Masked Email\" + 5. คัดลอกโทเค็นและวางด้านบน + 1. เข้าสู่ระบบแอป Fastmail บนเว็บ\n2. ไปที่ ตั้งค่า > ความเป็นส่วนตัวและความปลอดภัย\n3. คลิก การเชื่อมต่อ > โทเค็น API\n4. สร้างโทเค็นใหม่ที่มีขอบเขต \"Masked Email\"\n5. คัดลอกโทเค็นและวางด้านบน + + + อีเมลซ่อน + ค้นหาอีเมล… + ล้างการค้นหา + กรองอีเมล + สร้าง + สร้างอีเมลซ่อนใหม่ + ไม่พบอีเมลซ่อน + ออกจากระบบ + การตั้งค่า + ทั้งหมด + เปิดใช้งาน + ปิดใช้งาน + ลบแล้ว + + + สร้างอีเมลซ่อน + คำนำหน้าอีเมล (ไม่บังคับ) + เช่น mysite_shopping + สูงสุด 64 ตัวอักษร: ตัวพิมพ์เล็ก, ตัวเลข, ขีดล่าง + โดเมนที่เกี่ยวข้อง (ไม่บังคับ) + เช่น example.com + คำอธิบาย (ไม่บังคับ) + เช่น บัญชีช้อปปิ้ง + URL (ไม่บังคับ) + เช่น https://example.com + สถานะเริ่มต้น + สร้างอีเมลซ่อน + สร้างแล้ว: %s + คัดลอก + กลับ + + + รายละเอียดอีเมล + คัดลอกที่อยู่อีเมล + ลบอีเมล + สถานะ: + สร้างโดย: %s + สร้างเมื่อ: %s + ข้อความล่าสุด: %s + เปิดใช้งาน + ปิดใช้งาน + แก้ไขรายละเอียด + คำอธิบาย + โดเมนที่เกี่ยวข้อง + URL + บันทึกการเปลี่ยนแปลง + อัปเดตสำเร็จ + ลบสำเร็จ + คัดลอกไปยังคลิปบอร์ดแล้ว + ลบอีเมลซ่อน + คุณแน่ใจหรือไม่ว่าต้องการลบอีเมลซ่อนนี้? การกระทำนี้ไม่สามารถย้อนกลับได้ + ลบ + ยกเลิก + + + เปิดใช้งาน + ปิดใช้งาน + ลบแล้ว + รอดำเนินการ + + + ไม่สามารถโหลดอีเมลได้ + ลองอีกครั้ง + เกิดข้อผิดพลาด + + + การตั้งค่า + ภาษา + เลือกภาษาแอป + ค่าเริ่มต้นของระบบ + ติดต่อและความคิดเห็น + ส่งความคิดเห็นให้เรา + ออกจากระบบ + ออกจากบัญชีของคุณ + เวอร์ชัน %s + เลือกภาษา + ความคิดเห็น FastMask + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml new file mode 100644 index 0000000..a5dbe50 --- /dev/null +++ b/app/src/main/res/values-tr/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Fastmail Maskeli E-posta Yöneticisi + API Jetonu + Fastmail API jetonunuzu girin + Giriş Yap + Jetonu göster + Jetonu gizle + API jetonunuzu nasıl alırsınız: + 1. Fastmail web uygulamasına giriş yapın + 2. Ayarlar > Gizlilik ve Güvenlik\'e gidin + 3. Entegrasyonlar > API Jetonları\'na tıklayın + 4. \"Masked Email\" kapsamında yeni bir jeton oluşturun + 5. Jetonu kopyalayın ve yukarıya yapıştırın + 1. Fastmail web uygulamasına giriş yapın\n2. Ayarlar > Gizlilik ve Güvenlik\'e gidin\n3. Entegrasyonlar > API Jetonları\'na tıklayın\n4. \"Masked Email\" kapsamında yeni bir jeton oluşturun\n5. Jetonu kopyalayın ve yukarıya yapıştırın + + + Maskeli E-postalar + E-posta ara… + Aramayı temizle + E-postaları filtrele + Oluştur + Yeni maskeli e-posta oluştur + Maskeli e-posta bulunamadı + Çıkış Yap + Ayarlar + Tümü + Etkin + Devre Dışı + Silinmiş + + + Maskeli E-posta Oluştur + E-posta öneki (isteğe bağlı) + örn. sitem_alisveris + Maks 64 karakter: küçük harfler, rakamlar, alt çizgiler + İlişkili alan adı (isteğe bağlı) + örn. ornek.com + Açıklama (isteğe bağlı) + örn. Alışveriş hesabı + URL (isteğe bağlı) + örn. https://ornek.com + Başlangıç durumu + Maskeli E-posta Oluştur + Oluşturuldu: %s + Kopyala + Geri + + + E-posta Detayları + E-posta adresini kopyala + E-postayı sil + Durum: + Oluşturan: %s + Oluşturulma: %s + Son mesaj: %s + Etkinleştir + Devre dışı bırak + Detayları Düzenle + Açıklama + İlişkili alan adı + URL + Değişiklikleri Kaydet + Başarıyla güncellendi + Başarıyla silindi + Panoya kopyalandı + Maskeli E-postayı Sil + Bu maskeli e-postayı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz. + Sil + İptal + + + Etkin + Devre Dışı + Silinmiş + Beklemede + + + E-postalar yüklenemedi + Tekrar Dene + Bir hata oluştu + + + Ayarlar + Dil + Uygulama dilini seçin + Sistem Varsayılanı + İletişim ve Geri Bildirim + Geri bildiriminizi gönderin + Çıkış Yap + Hesabınızdan çıkış yapın + Sürüm %s + Dil Seçin + FastMask Geri Bildirimi + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000..b5c69e3 --- /dev/null +++ b/app/src/main/res/values-uk/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Менеджер масок email для Fastmail + API токен + Введіть ваш API токен Fastmail + Увійти + Показати токен + Приховати токен + Як отримати API токен: + 1. Увійдіть у веб-застосунок Fastmail + 2. Перейдіть до Налаштування > Конфіденційність і безпека + 3. Натисніть Інтеграції > API токени + 4. Створіть новий токен з правами \"Masked Email\" + 5. Скопіюйте токен і вставте вище + 1. Увійдіть у веб-застосунок Fastmail\n2. Перейдіть до Налаштування > Конфіденційність і безпека\n3. Натисніть Інтеграції > API токени\n4. Створіть новий токен з правами \"Masked Email\"\n5. Скопіюйте токен і вставте вище + + + Маски email + Пошук email… + Очистити пошук + Фільтрувати email + Створити + Створити нову маску email + Маски email не знайдено + Вийти + Налаштування + Усі + Активні + Вимкнені + Видалені + + + Створити маску email + Префікс email (необов\'язково) + напр., mysite_shopping + Макс. 64 символи: малі літери, цифри, підкреслення + Пов\'язаний домен (необов\'язково) + напр., example.com + Опис (необов\'язково) + напр., Акаунт для покупок + URL (необов\'язково) + напр., https://example.com + Початковий стан + Створити маску email + Створено: %s + Копіювати + Назад + + + Деталі email + Копіювати адресу email + Видалити email + Статус: + Створено: %s + Створено: %s + Останнє повідомлення: %s + Увімкнути + Вимкнути + Редагувати + Опис + Пов\'язаний домен + URL + Зберегти зміни + Успішно оновлено + Успішно видалено + Скопійовано до буфера обміну + Видалити маску email + Ви впевнені, що хочете видалити цю маску email? Цю дію не можна скасувати. + Видалити + Скасувати + + + Активна + Вимкнена + Видалена + Очікування + + + Не вдалося завантажити email + Повторити + Сталася помилка + + + Налаштування + Мова + Вибрати мову застосунку + Системна за замовчуванням + Зв\'язок та відгуки + Надішліть нам відгук + Вийти + Вийти з акаунту + Версія %s + Вибрати мову + Відгук про FastMask + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000..41a4527 --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Quản lý Email ẩn danh Fastmail + Mã API + Nhập mã API Fastmail của bạn + Đăng nhập + Hiện mã + Ẩn mã + Cách lấy mã API: + 1. Đăng nhập vào ứng dụng web Fastmail + 2. Vào Cài đặt > Quyền riêng tư & Bảo mật + 3. Nhấp vào Tích hợp > Mã API + 4. Tạo mã mới với phạm vi \"Masked Email\" + 5. Sao chép mã và dán ở trên + 1. Đăng nhập vào ứng dụng web Fastmail\n2. Vào Cài đặt > Quyền riêng tư & Bảo mật\n3. Nhấp vào Tích hợp > Mã API\n4. Tạo mã mới với phạm vi \"Masked Email\"\n5. Sao chép mã và dán ở trên + + + Email ẩn danh + Tìm kiếm email… + Xóa tìm kiếm + Lọc email + Tạo mới + Tạo email ẩn danh mới + Không tìm thấy email ẩn danh + Đăng xuất + Cài đặt + Tất cả + Đang hoạt động + Đã tắt + Đã xóa + + + Tạo email ẩn danh + Tiền tố email (tùy chọn) + vd: trangweb_muasam + Tối đa 64 ký tự: chữ thường, số, gạch dưới + Tên miền liên kết (tùy chọn) + vd: example.com + Mô tả (tùy chọn) + vd: Tài khoản mua sắm + URL (tùy chọn) + vd: https://example.com + Trạng thái ban đầu + Tạo email ẩn danh + Đã tạo: %s + Sao chép + Quay lại + + + Chi tiết email + Sao chép địa chỉ email + Xóa email + Trạng thái: + Tạo bởi: %s + Ngày tạo: %s + Tin nhắn cuối: %s + Bật + Tắt + Chỉnh sửa chi tiết + Mô tả + Tên miền liên kết + URL + Lưu thay đổi + Cập nhật thành công + Xóa thành công + Đã sao chép vào clipboard + Xóa email ẩn danh + Bạn có chắc muốn xóa email ẩn danh này? Hành động này không thể hoàn tác. + Xóa + Hủy + + + Đang hoạt động + Đã tắt + Đã xóa + Đang chờ + + + Không thể tải email + Thử lại + Đã xảy ra lỗi + + + Cài đặt + Ngôn ngữ + Chọn ngôn ngữ ứng dụng + Mặc định hệ thống + Liên hệ & Phản hồi + Gửi phản hồi cho chúng tôi + Đăng xuất + Đăng xuất khỏi tài khoản + Phiên bản %s + Chọn ngôn ngữ + Phản hồi FastMask + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000..43a68df --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,121 @@ + + + + FastMask + + + Fastmail 隐藏邮箱管理器 + API 令牌 + 输入您的 Fastmail API 令牌 + 登录 + 显示令牌 + 隐藏令牌 + 如何获取您的 API 令牌: + 1. 登录 Fastmail 网页版 + 2. 前往 设置 > 隐私与安全 + 3. 点击 集成 > API 令牌 + 4. 创建一个具有"隐藏邮箱"权限的新令牌 + 5. 复制令牌并粘贴到上方 + 1. 登录 Fastmail 网页版\n2. 前往 设置 > 隐私与安全\n3. 点击 集成 > API 令牌\n4. 创建一个具有"隐藏邮箱"权限的新令牌\n5. 复制令牌并粘贴到上方 + + + 隐藏邮箱 + 搜索邮箱… + 清除搜索 + 筛选邮箱 + 创建 + 创建新的隐藏邮箱 + 未找到隐藏邮箱 + 退出登录 + 设置 + 全部 + 已启用 + 已禁用 + 已删除 + + + 创建隐藏邮箱 + 邮箱前缀(可选) + 例如:mysite_shopping + 最多64个字符:小写字母、数字、下划线 + 关联域名(可选) + 例如:example.com + 描述(可选) + 例如:购物账户 + 网址(可选) + 例如:https://example.com + 初始状态 + 创建隐藏邮箱 + 已创建:%s + 复制 + 返回 + + + 邮箱详情 + 复制邮箱地址 + 删除邮箱 + 状态: + 创建者:%s + 创建时间:%s + 最后消息:%s + 启用 + 禁用 + 编辑详情 + 描述 + 关联域名 + 网址 + 保存更改 + 更新成功 + 删除成功 + 已复制到剪贴板 + 删除隐藏邮箱 + 您确定要删除此隐藏邮箱吗?此操作无法撤销。 + 删除 + 取消 + + + 已启用 + 已禁用 + 已删除 + 待处理 + + + 加载邮箱失败 + 重试 + 发生错误 + + + 设置 + 语言 + 选择应用语言 + 跟随系统 + 联系与反馈 + 向我们发送反馈 + 退出登录 + 退出您的账户 + 版本 %s + 选择语言 + FastMask 反馈 + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ca6e0be..6b1b332 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,121 @@ + FastMask + + + Fastmail Masked Email Manager + API Token + Enter your Fastmail API token + Login + Show token + Hide token + How to get your API token: + 1. Log in to Fastmail web app + 2. Go to Settings > Privacy & Security + 3. Click on Integrations > API tokens + 4. Create a new token with \"Masked Email\" scope + 5. Copy the token and paste it above + 1. Log in to Fastmail web app\n2. Go to Settings > Privacy & Security\n3. Click on Integrations > API tokens\n4. Create a new token with \"Masked Email\" scope\n5. Copy the token and paste it above + + + Masked Emails + Search emails… + Clear search + Filter emails + Create + Create new masked email + No masked emails found + Logout + Settings + All + Enabled + Disabled + Deleted + + + Create Masked Email + Email Prefix (optional) + e.g., mysite_shopping + Max 64 chars: lowercase letters, numbers, underscores + Associated Domain (optional) + e.g., example.com + Description (optional) + e.g., Shopping account + URL (optional) + e.g., https://example.com + Initial State + Create Masked Email + Created: %s + Copy + Navigate back + + + Email Details + Copy email address + Delete email + Status: + Created by: %s + Created: %s + Last message: %s + Enable + Disable + Edit Details + Description + Associated Domain + URL + Save Changes + Updated successfully + Deleted successfully + Copied to clipboard + Delete Masked Email + Are you sure you want to delete this masked email? This action cannot be undone. + Delete + Cancel + + + Enabled + Disabled + Deleted + Pending + + + Failed to load emails + Retry + An error occurred + + + Settings + Language + Select app language + System Default + Contact & Feedback + Send us your feedback + Logout + Sign out of your account + Version %s + Select Language + FastMask Feedback + + + English + 中文 + Español + हिन्दी + العربية + Português + বাংলা + Русский + 日本語 + Français + Deutsch + 한국어 + Italiano + Türkçe + Tiếng Việt + Polski + Українська + Nederlands + ไทย + Bahasa Indonesia diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index ade0d80..4654318 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -1,4 +1,23 @@ + + + + + + + + + + + + + + + + + + +