M8: localisation groundwork - resource the file-kind labels, pseudo-locale check
This commit is contained in:
@@ -24,8 +24,22 @@ upload needs a **higher `versionCode`** than every previous upload.
|
||||
device where the user had never declined anything. Reworded so it is true whether access
|
||||
was declined or simply never granted.
|
||||
|
||||
- **The details sheet put untranslatable English on screen.** It rendered `FileKind.name`
|
||||
directly, so "Type" read `directory`, `archive`, `apk` — enum constants rather than
|
||||
labels, and impossible to translate. Every category now has a string resource and a
|
||||
`FileKind.labelRes()` mapping alongside the existing `OperationKind.titleRes()`.
|
||||
|
||||
### Added
|
||||
|
||||
- **M8 — localisation groundwork.** Every user-visible string lives in
|
||||
`res/values/strings.xml` with real `<plurals>`, and an audit of the Compose sources found
|
||||
exactly one genuine miss (above). The debug build type enables
|
||||
`isPseudoLocalesEnabled`, so running it in English (XA) renders resource-backed text
|
||||
accented and bracketed while anything hardcoded looks normal — a mechanical way to catch
|
||||
the next one. Confirmed the pseudo-locale reaches the debug APK and not the release.
|
||||
Byte units and the timestamp pattern stay unlocalised on purpose, documented in the
|
||||
README along with the pseudo-locale check.
|
||||
|
||||
- **M7 — on-device validation.** The app was run on a real Xiaomi M2101K7AG (Android 12,
|
||||
MIUI V140) rather than only emulator images. Verified on hardware: the first-run
|
||||
permission prompt, "All files access" granted while the app is alive (picked up on
|
||||
|
||||
@@ -12,10 +12,10 @@ no advertising or tracking SDKs.
|
||||
|
||||
## Status
|
||||
|
||||
**M7 — on-device validation: done.** Run on a real Xiaomi M2101K7AG (Android 12/MIUI). The
|
||||
permission flows, real-storage browsing, the text editor's read/write path and the
|
||||
storage-usage screen were all exercised on hardware, and two real defects were found and
|
||||
fixed. See [docs/EDGE_CASE_TESTING.md](docs/EDGE_CASE_TESTING.md).
|
||||
**M8 — localisation groundwork: done.** Every user-visible string is in resources with real
|
||||
plurals, the audit's one genuine miss (the details sheet showing an enum name) is fixed, and
|
||||
the debug build's pseudo-locale makes the check mechanical. See the README's *Localisation*
|
||||
section.
|
||||
|
||||
## Tech stack
|
||||
|
||||
@@ -99,6 +99,60 @@ access"** through Play's Permissions Declaration Form, and have the core purpose
|
||||
in its store description. [PRIVACY.md](PRIVACY.md) is written to match the shipped
|
||||
permissions.
|
||||
|
||||
## Localisation
|
||||
|
||||
All user-visible text lives in `app/src/main/res/values/strings.xml`, with real `<plurals>`
|
||||
wherever a count can be one, and the screen and the progress notification resolve their
|
||||
labels through `text/TextRes.kt` so the two cannot drift apart. **English is currently the
|
||||
only locale.**
|
||||
|
||||
Adding a language is then a file, not a refactor:
|
||||
|
||||
```
|
||||
app/src/main/res/values-de/strings.xml # same keys, translated values
|
||||
```
|
||||
|
||||
Deliberately **not** localised, and why:
|
||||
|
||||
- **Byte units** ("KB", "MB") — symbols, not words, and every file manager and the system
|
||||
Settings app show them the same way.
|
||||
- **The timestamp pattern** (`yyyy-MM-dd HH:mm`) — a fixed, sortable format; the zone is
|
||||
passed in explicitly rather than assumed.
|
||||
|
||||
### Checking that nothing was missed
|
||||
|
||||
Two things are easy to get wrong: a literal typed into a composable, and showing an enum
|
||||
constant directly (`FileKind.name` puts untranslatable English on screen — the details
|
||||
sheet used to do exactly that). The mechanical check is the pseudo-locale:
|
||||
|
||||
```bash
|
||||
./gradlew :app:assembleDebug
|
||||
# Settings > System > Languages > English (XA), then launch the debug build.
|
||||
```
|
||||
|
||||
The debug build type sets `isPseudoLocalesEnabled = true`, so every string that came from
|
||||
`strings.xml` renders accented and bracketed (`[Îmàgé]`). **Anything that looks normal is a
|
||||
string that never went through resources.** The release build is unaffected.
|
||||
|
||||
One caveat: **Google Play's automatic translation translates your store listing, not the app
|
||||
UI.** Translated UI text has to be supplied in `values-<locale>/`.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- **"Open with" and Share only work in the all-files mode.** A SAF path is relative to the
|
||||
granted tree rather than a filesystem path, so it cannot be handed to `FileProvider`.
|
||||
- **RAR and 7z are refused**, not supported — they need native code, which this project
|
||||
deliberately does not ship.
|
||||
- **Archive extraction does not restore original timestamps or permissions**, and there is
|
||||
no archive encryption or password support.
|
||||
- **Cancellation happens between entries**, so a single very large file finishes before a
|
||||
stop is honoured.
|
||||
- **View state is not persisted across process death** (`SavedStateHandle` is not wired up).
|
||||
- **Long-press multi-select could not be driven from adb** on the MIUI test device, so the
|
||||
compress/extract buttons want a manual pass (see
|
||||
[docs/EDGE_CASE_TESTING.md](docs/EDGE_CASE_TESTING.md)).
|
||||
- The display name is still the working title — decided before the first upload (M10).
|
||||
|
||||
## Licence
|
||||
|
||||
Proprietary — all rights reserved.
|
||||
|
||||
@@ -72,6 +72,10 @@ android {
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
// Pseudo-locale (en-XA): every string from resources is rendered accented and
|
||||
// wrapped in brackets, so any text that comes out unchanged is text that never
|
||||
// went through `strings.xml`. Set the device language to English (XA) to see it.
|
||||
isPseudoLocalesEnabled = true
|
||||
}
|
||||
release {
|
||||
if (hasReleaseSigning) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.softwareapp_hb.filemanager.text
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import de.softwareapp_hb.filemanager.R
|
||||
import de.softwareapp_hb.filemanager.core.FileKind
|
||||
import de.softwareapp_hb.filemanager.domain.OperationKind
|
||||
|
||||
/**
|
||||
@@ -19,3 +20,22 @@ fun OperationKind.titleRes(): Int = when (this) {
|
||||
OperationKind.COMPRESS -> R.string.operation_compress_title
|
||||
OperationKind.EXTRACT -> R.string.operation_extract_title
|
||||
}
|
||||
|
||||
/**
|
||||
* The human name of a file category, for the "Type" row of the details sheet.
|
||||
*
|
||||
* `FileKind.name` is an enum constant, not a label: showing it directly would put
|
||||
* untranslatable English in front of the user.
|
||||
*/
|
||||
@StringRes
|
||||
fun FileKind.labelRes(): Int = when (this) {
|
||||
FileKind.DIRECTORY -> R.string.browser_details_folder
|
||||
FileKind.IMAGE -> R.string.browser_kind_image
|
||||
FileKind.VIDEO -> R.string.browser_kind_video
|
||||
FileKind.AUDIO -> R.string.browser_kind_audio
|
||||
FileKind.DOCUMENT -> R.string.browser_kind_document
|
||||
FileKind.ARCHIVE -> R.string.browser_kind_archive
|
||||
FileKind.APK -> R.string.browser_kind_apk
|
||||
FileKind.TEXT -> R.string.browser_kind_text
|
||||
FileKind.OTHER -> R.string.browser_kind_other
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import de.softwareapp_hb.filemanager.R
|
||||
import de.softwareapp_hb.filemanager.core.FileEntry
|
||||
import de.softwareapp_hb.filemanager.core.FileSize
|
||||
import de.softwareapp_hb.filemanager.core.Timestamps
|
||||
import de.softwareapp_hb.filemanager.text.labelRes
|
||||
|
||||
/** Name, location, size, type and modified date for one entry. */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -39,7 +40,7 @@ fun DetailsSheet(entry: FileEntry, onDismiss: () -> Unit) {
|
||||
FileSize.format(entry.sizeBytes)
|
||||
},
|
||||
)
|
||||
DetailLine(stringResource(R.string.browser_details_kind), entry.kind.name.lowercase())
|
||||
DetailLine(stringResource(R.string.browser_details_kind), stringResource(entry.kind.labelRes()))
|
||||
Timestamps.format(entry.lastModifiedMillis).takeIf { it.isNotEmpty() }?.let { modified ->
|
||||
DetailLine(stringResource(R.string.browser_details_modified), modified)
|
||||
}
|
||||
|
||||
@@ -93,6 +93,16 @@
|
||||
<string name="browser_details_modified">Modified</string>
|
||||
<string name="browser_details_folder">Folder</string>
|
||||
|
||||
<!-- ===== File categories (the "Type" row of the details sheet) ===== -->
|
||||
<string name="browser_kind_image">Image</string>
|
||||
<string name="browser_kind_video">Video</string>
|
||||
<string name="browser_kind_audio">Audio</string>
|
||||
<string name="browser_kind_document">Document</string>
|
||||
<string name="browser_kind_archive">Archive</string>
|
||||
<string name="browser_kind_apk">App package</string>
|
||||
<string name="browser_kind_text">Text</string>
|
||||
<string name="browser_kind_other">File</string>
|
||||
|
||||
<!-- ===== Browser: delete + conflict ===== -->
|
||||
<string name="browser_delete_title">Delete permanently?</string>
|
||||
<plurals name="browser_delete_body">
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (propo
|
||||
| M5 | Analysis, archives, viewers | ✅ | `domain/UsageAnalysis` measures recursive folder sizes and finds the largest files, shown by a bar-per-row storage-usage screen; `domain/ArchiveEngine` creates ZIP and extracts ZIP/TAR/TAR.GZ/GZ/BZ2/XZ using `java.util.zip` + Commons Compress (no native code), with **every entry name filtered through `core/ArchivePath` against zip-slip**; a Coil-backed image viewer, thumbnails in the list and grid, a text editor, and compress/extract in the selection bar. **141 JVM tests** total (26 added here) |
|
||||
| M6 | Release config & Play readiness | ✅ | Upload keystore generated (4096-bit RSA, `CN=Hadrian Burkhardt`) and wired through the git-ignored `keystore.properties`; `bundleRelease` + `assembleRelease` verified — **4 682 814-byte `.aab`** (`sha256 9014627a…`), 2 081 955-byte APK signed by the upload key, R8 mapping kept. [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) (incl. the **Permissions Declaration Form** for "All files access", the Data safety answers and the shipped-permission audit) and [PRIVACY.md](../PRIVACY.md) written for a restricted-permission app. Shipped permission set audited: **no `INTERNET`** |
|
||||
| M7 | On-device validation & hardening | ✅ | Run on a real **Xiaomi M2101K7AG** (Android 12 / MIUI V140) — see [EDGE_CASE_TESTING](EDGE_CASE_TESTING.md). Verified on hardware: the first-run permission prompt, granting "All files access" while the app is alive (picked up on resume), browsing real storage, the text editor's read **and** write path (independently checked with `adb shell cat`), the storage-usage screen, landscape layout, and **graceful degradation when access is revoked mid-use**. Found and fixed two real defects: misleading first-run copy ("Full access was declined" on a device where nothing had been declined), and a usage screen that stayed blank for **~45–60 s** on a 55 GB volume — now one listing per directory instead of a `stat` per node, with rows streamed out as they are measured. **142 JVM tests** |
|
||||
| M8 | Localisation groundwork | ⏳ | every user-visible string in `res/values/strings.xml` with real `<plurals>`; `text/` mapping |
|
||||
| M8 | Localisation groundwork | ✅ | Every user-visible string is in `res/values/strings.xml` with real `<plurals>`, and `text/TextRes.kt` maps **both** `OperationKind` and `FileKind` onto resources so the screen, the notification and the details sheet cannot drift. The audit found a genuine miss: the details sheet rendered `FileKind.name`, i.e. untranslatable English — now a resource. `isPseudoLocalesEnabled = true` on the debug build makes "did every string come from resources?" mechanically checkable; verified `en-rXA` is present in the debug APK and the flag is debug-only. Byte units and the timestamp pattern are deliberately unlocalised, and the README says why. **142 JVM tests** |
|
||||
| M9 | Monetisation | ⏳ | **one-time paid** — price set only in the Play Console before the first upload; no Billing code and no `BILLING` permission |
|
||||
| M10 | Store assets | ⏳ | icon/feature-graphic generators, per-form-factor screenshots, [STORE_LISTING](STORE_LISTING.md); display name confirmed |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user