aqlab 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aqlab-0.1.4/LICENSE +21 -0
- aqlab-0.1.4/PKG-INFO +453 -0
- aqlab-0.1.4/README.md +431 -0
- aqlab-0.1.4/aqlab.egg-info/PKG-INFO +453 -0
- aqlab-0.1.4/aqlab.egg-info/SOURCES.txt +11 -0
- aqlab-0.1.4/aqlab.egg-info/dependency_links.txt +1 -0
- aqlab-0.1.4/aqlab.egg-info/entry_points.txt +2 -0
- aqlab-0.1.4/aqlab.egg-info/top_level.txt +1 -0
- aqlab-0.1.4/aqlab.py +1107 -0
- aqlab-0.1.4/pyproject.toml +34 -0
- aqlab-0.1.4/setup.cfg +4 -0
- aqlab-0.1.4/tests/test_aqlab.py +556 -0
- aqlab-0.1.4/tests/test_readme.py +80 -0
aqlab-0.1.4/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gilgamesh-110
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
aqlab-0.1.4/PKG-INFO
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aqlab
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Make an Android app Arabic with correct RTL: layout direction, attribute migration and bidi linting in one command.
|
|
5
|
+
Author: Gilgamesh-110
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Gilgamesh-110/aqlab
|
|
8
|
+
Project-URL: Issues, https://github.com/Gilgamesh-110/aqlab/issues
|
|
9
|
+
Keywords: android,arabic,rtl,i18n,localization,apk,bidi,right-to-left
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Natural Language :: Arabic
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Localization
|
|
17
|
+
Classifier: Topic :: Software Development :: Internationalization
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
<div align="center">
|
|
24
|
+
|
|
25
|
+
<img src="assets/banner.svg" alt="aqlab — Arabic & RTL localization for Android APKs" width="100%">
|
|
26
|
+
|
|
27
|
+
**Flip any Android app to Arabic — RTL-correct — in one command.**
|
|
28
|
+
|
|
29
|
+
[](https://github.com/Gilgamesh-110/aqlab/actions/workflows/ci.yml)
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
[](https://www.python.org/downloads/)
|
|
32
|
+
[](#requirements)
|
|
33
|
+
[](CONTRIBUTING.md)
|
|
34
|
+
[](CHANGELOG.md)
|
|
35
|
+
[](https://github.com/Gilgamesh-110/aqlab/releases/latest)
|
|
36
|
+
[](#requirements)
|
|
37
|
+
|
|
38
|
+
[العربية](README.ar.md) · **English**
|
|
39
|
+
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
Translating an app into Arabic is the easy half. The half almost everybody gets
|
|
45
|
+
wrong is the interface: the drawer stays on the left, the back arrow still points
|
|
46
|
+
left, progress bars fill left-to-right, `%d` placeholders go missing, and
|
|
47
|
+
punctuation lands on the wrong side of the line.
|
|
48
|
+
|
|
49
|
+
**`aqlab` does the part that is not translation** — layout direction, attribute
|
|
50
|
+
migration, Unicode bidi hazards, and Android resource escaping.
|
|
51
|
+
|
|
52
|
+
<p align="center">
|
|
53
|
+
<img src="assets/features.svg" alt="layout direction, property migration, bidi risk scan, resource escaping" width="100%">
|
|
54
|
+
</p>
|
|
55
|
+
|
|
56
|
+
<p align="center">
|
|
57
|
+
<img src="assets/demo.svg" alt="terminal session: scan, rtl, lint" width="100%">
|
|
58
|
+
</p>
|
|
59
|
+
|
|
60
|
+
## Try it in 5 seconds
|
|
61
|
+
|
|
62
|
+
No APK and no Android SDK needed for this part — there is a small decoded project
|
|
63
|
+
in the repo that deliberately contains the traps:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
git clone https://github.com/Gilgamesh-110/aqlab && cd aqlab
|
|
67
|
+
python aqlab.py rtl examples/demo-project
|
|
68
|
+
python aqlab.py apply examples/demo-project --tsv examples/ar.tsv
|
|
69
|
+
python aqlab.py lint examples/demo-project
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
[PATCH] AndroidManifest.xml: android:supportsRtl="true"
|
|
74
|
+
[PATCH] res/layout/activity_main.xml: 11 attribute(s)
|
|
75
|
+
[PATCH] res/layout/list_item.xml: 6 attribute(s)
|
|
76
|
+
[PATCH] res/values/styles.xml: 2 attribute(s)
|
|
77
|
+
[DONE] 4 file(s) updated
|
|
78
|
+
[DONE] 12 translation(s) -> examples/demo-project/res/values-ar/strings.xml
|
|
79
|
+
[FINDING] lint res/values-ar: 12/12 translated (100.0%)
|
|
80
|
+
[ERROR] plurals/selected_count: missing Arabic quantities: few (3-10), many (11-99) — those counts fall back to `other`
|
|
81
|
+
[WARN] plurals/selected_count: no zero (0), two (2) form — those counts use `other`
|
|
82
|
+
[FAIL] 1 error(s), 1 warning(s)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
That last line is the reason this tool exists. The demo's Arabic is **100%
|
|
86
|
+
translated and still wrong** at every count above 2 — it says `3 عنصر` where
|
|
87
|
+
Arabic needs `3 عناصر`, `11 عنصراً` above ten and `لا عناصر` at zero. There is no
|
|
88
|
+
missing string, no build error and nothing an ordinary linter reports.
|
|
89
|
+
|
|
90
|
+
It contains a layout that declares `paddingLeft` **and** `paddingStart`, a
|
|
91
|
+
`<style>` that carries the direction in an `<item>` value, and a
|
|
92
|
+
`?listPreferredItemPaddingLeft` theme reference — the three things that break a
|
|
93
|
+
naive migration.
|
|
94
|
+
|
|
95
|
+
<p align="center">
|
|
96
|
+
<img src="assets/before-after.svg" alt="the same screen before and after, with the attributes that changed" width="100%">
|
|
97
|
+
</p>
|
|
98
|
+
|
|
99
|
+
## Contents
|
|
100
|
+
|
|
101
|
+
- [Try it in 5 seconds](#try-it-in-5-seconds)
|
|
102
|
+
- [Why](#why)
|
|
103
|
+
- [Install](#install)
|
|
104
|
+
- [Quick start](#quick-start)
|
|
105
|
+
- [Commands](#commands)
|
|
106
|
+
- [What it fixes](#what-it-fixes)
|
|
107
|
+
- [The part nobody else does: bidi linting](#the-part-nobody-else-does-bidi-linting)
|
|
108
|
+
- [What it does *not* do](#what-it-does-not-do)
|
|
109
|
+
- [How this relates to other tools](#how-this-relates-to-other-tools)
|
|
110
|
+
- [Tested on](#tested-on)
|
|
111
|
+
- [FAQ](#faq)
|
|
112
|
+
- [Requirements](#requirements)
|
|
113
|
+
- [Docs](#docs)
|
|
114
|
+
- [Contributing](#contributing)
|
|
115
|
+
- [License](#license)
|
|
116
|
+
|
|
117
|
+
## Why
|
|
118
|
+
|
|
119
|
+
Arabic is written right-to-left. Android has had a first-class mechanism for this
|
|
120
|
+
since API 17 — logical `start`/`end` attributes plus `android:supportsRtl` — and
|
|
121
|
+
yet a large share of shipped apps never turned it on, or turned it on and left
|
|
122
|
+
hundreds of hardcoded `left`/`right` attributes behind.
|
|
123
|
+
|
|
124
|
+
Doing it by hand means auditing every layout, every menu, every style, and only
|
|
125
|
+
finding the mistakes when you look at the app running in Arabic. `aqlab`
|
|
126
|
+
automates the mechanical 95% and **lints the rest**, so the first Arabic build
|
|
127
|
+
you look at is already correct.
|
|
128
|
+
|
|
129
|
+
## Install
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
pipx install git+https://github.com/Gilgamesh-110/aqlab # or
|
|
133
|
+
curl -O https://raw.githubusercontent.com/Gilgamesh-110/aqlab/main/aqlab.py
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
One file, standard library only. It drives tools you already have:
|
|
137
|
+
|
|
138
|
+
| tool | how it is found |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `apktool` | `AQLAB_APKTOOL=/path/to/apktool.jar`, else `apktool` on `PATH` |
|
|
141
|
+
| `aapt2`, `zipalign`, `apksigner` | `ANDROID_HOME` / `AQLAB_SDK`, else `PATH` |
|
|
142
|
+
|
|
143
|
+
Per-tool overrides also work: `AQLAB_AAPT2`, `AQLAB_ZIPALIGN`, `AQLAB_APKSIGNER`,
|
|
144
|
+
`AQLAB_JAVA`.
|
|
145
|
+
|
|
146
|
+
## Quick start
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
aqlab scan app.apk # what is the state of this app?
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
[FINDING] apk: app.apk
|
|
154
|
+
package : com.example.app
|
|
155
|
+
label : 'Example'
|
|
156
|
+
locales : 87 -> --_--, af, am, ar, as, az, be, bg, bn, bs, ca, cs, da, de
|
|
157
|
+
arabic present : yes
|
|
158
|
+
supportsRtl : no (or not detectable in binary XML)
|
|
159
|
+
native libs : 6 (renaming packages in native apps can break self-checks)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
aqlab decode app.apk -o work # unpack
|
|
164
|
+
aqlab rtl work --dry-run # preview the migration
|
|
165
|
+
aqlab rtl work # apply it
|
|
166
|
+
aqlab scaffold work # -> work/values-ar.tsv (table stays out of res/)
|
|
167
|
+
# ... translate the last column (DeepL, an LLM, or a human) ...
|
|
168
|
+
aqlab apply work --tsv work/values-ar.tsv
|
|
169
|
+
aqlab lint work # exits non-zero on real errors
|
|
170
|
+
aqlab build work --ks release.keystore --ks-alias release
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Or all of it at once:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
aqlab all app.apk --tsv ar.tsv --ks release.keystore --ks-alias release
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Commands
|
|
180
|
+
|
|
181
|
+
| command | what it does |
|
|
182
|
+
|---|---|
|
|
183
|
+
| `scan <apk\|dir>` | read-only report: locales, `supportsRtl`, remaining `left/right`, coverage. `--json` for tooling. |
|
|
184
|
+
| `decode <apk>` | unpack an APK into an editable project. Skips `smali/` by default — see below. |
|
|
185
|
+
| `rtl <dir>` | set `android:supportsRtl` and migrate `left/right` → `start/end` across `res/**`. `--dry-run` to preview. |
|
|
186
|
+
| `scaffold <dir>` | create `res/values-ar/` and a `values-ar.tsv` translation table. Every `values/*.xml` is read, not just `strings.xml`. |
|
|
187
|
+
| `apply <dir> --tsv` | import a translated table back into `values-ar/`, each entry into the file its source came from |
|
|
188
|
+
| `lint <dir>` | bidi hazards, escaping bugs, format-specifier drift, Arabic plural structure, coverage. Non-zero exit on errors. |
|
|
189
|
+
| `build <dir>` | `apktool b` → `zipalign` → `apksigner sign` → `apksigner verify` |
|
|
190
|
+
| `all <apk>` | the whole pipeline |
|
|
191
|
+
|
|
192
|
+
### Speed, and why `decode` skips `smali/`
|
|
193
|
+
|
|
194
|
+
Disassembling the bytecode is most of the decode time, and this tool never edits
|
|
195
|
+
code — so it is skipped. `apktool` records the source APK in `apktool.yml` and
|
|
196
|
+
copies the original `classes*.dex` back on build **byte for byte**; the end-to-end
|
|
197
|
+
run below was verified with `sha256sum` on every dex, and the built APK passes
|
|
198
|
+
`apksigner verify` and `zipalign -c`.
|
|
199
|
+
|
|
200
|
+
Measured on a real 17 MB app — an 8,447-file tree, 125 layouts, 210 `values/`
|
|
201
|
+
files, 5 dex files:
|
|
202
|
+
|
|
203
|
+
| step | with `smali/` | default | what changed |
|
|
204
|
+
|---|---|---|---|
|
|
205
|
+
| `decode` | 52.1s | **10.0s** | skip the disassembly |
|
|
206
|
+
| `build` | 65.3s | **27.0s** | nothing to re-assemble |
|
|
207
|
+
| `rtl` | 6.6s | **4.3s** | tags that cannot migrate are skipped |
|
|
208
|
+
| **whole pipeline** | ~117s | **51s** | signed and verified |
|
|
209
|
+
|
|
210
|
+
`rtl` got faster because the rename tables used to run over *every attribute of
|
|
211
|
+
every element*. A tag is now skipped unless one of the names the tables know
|
|
212
|
+
about appears in it, so `layout_width` / `text` / `id` tags cost one substring
|
|
213
|
+
search. That cut pure-Python calls from 692k to 288k with byte-identical output.
|
|
214
|
+
|
|
215
|
+
Skipping `smali/` also shrinks the working tree from **123 MB across 8,447 files
|
|
216
|
+
to 47 MB across 1,197** — so every later copy, grep, archive and rebuild works on
|
|
217
|
+
a tree an order of magnitude smaller.
|
|
218
|
+
|
|
219
|
+
Need the smali tree after all? `--with-sources` on `decode` or `all`.
|
|
220
|
+
|
|
221
|
+
## What it fixes
|
|
222
|
+
|
|
223
|
+
* `android:supportsRtl="true"` on `<application>`
|
|
224
|
+
* `paddingLeft/Right` → `paddingStart/End`, and the same for `layout_margin*`,
|
|
225
|
+
`drawable*`, `layout_align*`, `layout_alignParent*`, `layout_to*Of`
|
|
226
|
+
* ConstraintLayout `app:layout_constraint{Left,Right}_to{Left,Right}Of` → `{Start,End}`
|
|
227
|
+
* `gravity` / `layout_gravity` / `foregroundGravity` value tokens
|
|
228
|
+
* `textAlignment` → `viewStart` / `viewEnd`
|
|
229
|
+
* `<item name="android:paddingLeft">` entries inside `<style>` blocks
|
|
230
|
+
* theme references that would otherwise keep resolving to the LTR value
|
|
231
|
+
(`?listPreferredItemPaddingLeft` → `?…Start`)
|
|
232
|
+
* Android resource escaping: `'`, leading `@`/`?`, and significant
|
|
233
|
+
leading/trailing spaces
|
|
234
|
+
|
|
235
|
+
Every rename is guarded: when a tag already declares the `start`/`end` twin, the
|
|
236
|
+
old attribute is **removed** instead of duplicated. That guard is the difference
|
|
237
|
+
between a project that builds and one that dies inside `aapt2` with
|
|
238
|
+
`error: duplicate attribute` — and it is why this works on AppCompat and Material
|
|
239
|
+
layouts, which routinely carry both.
|
|
240
|
+
|
|
241
|
+
## The part nobody else does: bidi linting
|
|
242
|
+
|
|
243
|
+
Arabic runs right-to-left. Numbers, Latin words, file paths and `%1$s`
|
|
244
|
+
placeholders inside it run left-to-right. Where the two meet, the Unicode
|
|
245
|
+
Bidirectional Algorithm reorders runs and punctuation ends up on the wrong side.
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
text: المجلد test.
|
|
249
|
+
renders: المجلد test. the period sits right of "test"; it should sit left
|
|
250
|
+
fix: المجلد test\u200f. U+200F RIGHT-TO-LEFT MARK pins it in place
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`aqlab lint` finds that, plus the failures that actually break the build:
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
[FINDING] lint res/values-ar: 148/190 translated (77.9%)
|
|
257
|
+
[WARN] abc_menu_ctrl_shortcut_label: identical to source and contains Latin — probably untranslated
|
|
258
|
+
[ERROR] discount: bare `%` — must be `%%` or a format specifier, aapt will reject it
|
|
259
|
+
[ERROR] welcome: format specifiers differ: source ['%1$s'] vs arabic ['%s']
|
|
260
|
+
[ERROR] note: unescaped apostrophe — use \' inside Android string resources
|
|
261
|
+
[ERROR] folder: trailing Latin/digit run before punctuation — needs U+200F (RLM)
|
|
262
|
+
[FAIL] 4 error(s), 1 warning(s)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
| rule | level |
|
|
266
|
+
|---|---|
|
|
267
|
+
| format specifiers differ from the source | error |
|
|
268
|
+
| bare `%` that `aapt` will reject | error |
|
|
269
|
+
| unescaped `'` inside a string resource | error |
|
|
270
|
+
| value starts with `@` or `?` | error |
|
|
271
|
+
| markup tags (`<b>`, `<xliff:g>`) differ from the source | error |
|
|
272
|
+
| trailing Latin/digit run before punctuation (needs U+200F) | error |
|
|
273
|
+
| an Arabic `<plurals>` missing `other`, `few` or `many` | error |
|
|
274
|
+
| value identical to the source and contains Latin | warn |
|
|
275
|
+
| no Arabic characters in the Arabic value | warn |
|
|
276
|
+
| leading/trailing space mismatch | warn |
|
|
277
|
+
| `few` / `many` / `other` carrying identical text | warn |
|
|
278
|
+
| an Arabic `<plurals>` missing `zero` or `two` | warn |
|
|
279
|
+
| mixed Arabic + Latin in one string | info |
|
|
280
|
+
|
|
281
|
+
Exit code is non-zero on errors, so it drops straight into CI.
|
|
282
|
+
|
|
283
|
+
## The other part nobody does: Arabic has six plurals, not two
|
|
284
|
+
|
|
285
|
+
English has two plural forms (`one`, `other`). Arabic has **six**, and the two
|
|
286
|
+
that cover almost every count — `few` (3–10) and `many` (11–99) — are exactly the
|
|
287
|
+
two an English source cannot hand you:
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
count 0 1 2 3-10 11-99 100, 101, …
|
|
291
|
+
zero one two few many other
|
|
292
|
+
لا عنصر عنصران %d عناصر %d عنصراً %d عنصر
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
So the standard outcome is an Arabic `<plurals>` that mirrors the English one —
|
|
296
|
+
two categories, both in Arabic words. It compiles. It links. **Nothing in the
|
|
297
|
+
build says a word.** And then every count from 2 upward falls through to `other`:
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
3 عنصر ← should be 3 عناصر
|
|
301
|
+
15 عنصر ← should be 15 عنصراً
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`aqlab lint` reads `<plurals>` as a structure and reports it:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
[ERROR] plurals/n_items: missing Arabic quantities: few (3-10), many (11-99)
|
|
308
|
+
— those counts fall back to `other`
|
|
309
|
+
[WARN] plurals/n_items: no zero (0), two (2) form — those counts use `other`
|
|
310
|
+
[WARN] plurals/n_items: few, many, other carry identical text
|
|
311
|
+
— the plural forms are not actually pluralised
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Three things worth knowing about how this behaves:
|
|
315
|
+
|
|
316
|
+
* **Dropping the count in `zero`/`one`/`two` is not a finding.** `عنصر واحد` and
|
|
317
|
+
`عنصران` are correct Arabic; only a *invented* placeholder is an error.
|
|
318
|
+
* **`other` missing is not a build failure.** Verified with `aapt2` — it compiles
|
|
319
|
+
and links a locale whose `<plurals>` has no `other`. That is worse, not better:
|
|
320
|
+
the counts simply have no Arabic value and nothing warns. This check is the only
|
|
321
|
+
thing that will.
|
|
322
|
+
* **It reads every file in the locale folder, not just `strings.xml`.** Plurals
|
|
323
|
+
normally live in their own `plurals.xml` — AOSP and most Material apps ship them
|
|
324
|
+
that way, so a strings-only check would miss exactly the entries it exists for.
|
|
325
|
+
|
|
326
|
+
### Supplying the missing forms
|
|
327
|
+
|
|
328
|
+
The translation table already carries a row for **all six categories**, including
|
|
329
|
+
the ones an English source has no line for — otherwise there is nowhere to write
|
|
330
|
+
them, and `apply` would have nothing to read back:
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
aqlab scaffold work # work/values-ar.tsv
|
|
334
|
+
n_items|zero ← blank: Arabic-only categories get a row of their own
|
|
335
|
+
n_items|one %d item
|
|
336
|
+
n_items|two
|
|
337
|
+
n_items|few
|
|
338
|
+
n_items|many
|
|
339
|
+
n_items|other %d items
|
|
340
|
+
|
|
341
|
+
aqlab apply work --tsv work/values-ar.tsv
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Each entry goes back into the file its source came from, so an app that keeps
|
|
345
|
+
`values/plurals.xml` gets `values-ar/plurals.xml`. Nothing is funnelled into
|
|
346
|
+
`strings.xml`, because a `<plurals>` name declared in two files of one locale is
|
|
347
|
+
a duplicate-resource error. The table itself lives at `work/values-ar.tsv`, not
|
|
348
|
+
inside `res/` — a `.tsv` under `res/` makes `aapt2 compile --dir res` fail.
|
|
349
|
+
|
|
350
|
+
## What it does *not* do
|
|
351
|
+
|
|
352
|
+
* **It does not translate anything.** No machine translation, no AI. The last
|
|
353
|
+
column of the TSV is your job — the tool guarantees the mechanics survive once
|
|
354
|
+
it is filled.
|
|
355
|
+
* It does not touch strings compiled into DEX/smali or native code.
|
|
356
|
+
* It does not mirror images or vector assets with baked-in Latin text.
|
|
357
|
+
* It deliberately does not force `android:layoutDirection="rtl"` — that would
|
|
358
|
+
break every other locale in a multi-language app. `supportsRtl` with
|
|
359
|
+
`start`/`end` is the correct mechanism, and it is what Google's own
|
|
360
|
+
documentation specifies.
|
|
361
|
+
|
|
362
|
+
## How this relates to other tools
|
|
363
|
+
|
|
364
|
+
| tool | what it does | where it stops |
|
|
365
|
+
|---|---|---|
|
|
366
|
+
| **Android Lint** (`RtlHardcoded`, `RtlEnabled`) | flags hardcoded `left`/`right` in a **source** project, and Android Studio can auto-fix them | needs source and a working build. It cannot see an APK. Says nothing about bidi in translated strings or `%1$s` drift. |
|
|
367
|
+
| **Android Studio translation editor** | tracks which strings are translated | manages text, not direction. Does not touch a layout. |
|
|
368
|
+
| **`android:layoutDirection="rtl"`** | forces a subtree right-to-left | the common wrong fix — it breaks every other locale in a multi-language app. |
|
|
369
|
+
| **Machine translation** (DeepL, Google, an LLM) | translates the strings | will happily turn `%1$s` into `%s`, and cannot see a layout at all. |
|
|
370
|
+
| **Crowdin / Lokalise / Weblate** | orchestrates translators and review | upstream of the layout work; no RTL mechanics, no bidi linting. |
|
|
371
|
+
| **`apktool` alone** | gives you a decoded project | does none of the migration, and hands you the duplicate-attribute trap for free. |
|
|
372
|
+
| **`aqlab`** | sets `supportsRtl`, migrates the attributes, scaffolds and applies the translation table, lints bidi and resource escaping | operates on a **decoded APK** — no source needed. Does not translate anything. |
|
|
373
|
+
|
|
374
|
+
**When not to use this.** If you have the project source and a working build, use
|
|
375
|
+
Android Lint's `RtlHardcoded` and `RtlEnabled` checks — that is the right tool, it
|
|
376
|
+
runs in your build, and it is maintained by the people who make the platform.
|
|
377
|
+
`aqlab` exists for the other case: an APK and no source, or a source tree that no
|
|
378
|
+
longer builds.
|
|
379
|
+
|
|
380
|
+
## Tested on
|
|
381
|
+
|
|
382
|
+
Real APKs, end to end (`decode` → `rtl` → `build` → sign → verify) — not just
|
|
383
|
+
unit tests. `supportsRtl=true` is read back out of the **compiled** manifest, and
|
|
384
|
+
the output is checked with `apksigner verify` and `zipalign -c -v 4`:
|
|
385
|
+
|
|
386
|
+
| app | locales | rebuilt | signed | `supportsRtl` in compiled manifest |
|
|
387
|
+
|---|---|---|---|---|
|
|
388
|
+
| multi-language app (`com.gilgamesh.vip`) | 87 | yes | yes | ✅ `true` |
|
|
389
|
+
| utility app (`TS_Vip_v115`) | — | yes | yes | ✅ `true` |
|
|
390
|
+
| utility app (`bs_1.0`) | — | yes | yes | ✅ `true` |
|
|
391
|
+
|
|
392
|
+
The `rtl` pass is idempotent: running it a second time reports
|
|
393
|
+
`already RTL-clean — nothing to migrate` and writes nothing.
|
|
394
|
+
|
|
395
|
+
## FAQ
|
|
396
|
+
|
|
397
|
+
**A build failed with `error: duplicate attribute`.**
|
|
398
|
+
You are on a version before the twin guard. Update, or open an issue with the
|
|
399
|
+
file named in the error.
|
|
400
|
+
|
|
401
|
+
**It refuses to run with `minSdkVersion=15`.**
|
|
402
|
+
`start`/`end` resources do not exist before API 17. Raise `minSdkVersion`, or
|
|
403
|
+
pass `--force-min-sdk` knowing the build will then fail.
|
|
404
|
+
|
|
405
|
+
**The rebuilt app crashes, or detects that it was modified.**
|
|
406
|
+
Expected when the original verifies its signature or dex checksums natively — a
|
|
407
|
+
rebuild changes both. See [SECURITY.md](SECURITY.md).
|
|
408
|
+
|
|
409
|
+
**Does it work for Hebrew, Persian or Urdu?**
|
|
410
|
+
The mechanics are identical — `rtl`, `lint` and the attribute tables are
|
|
411
|
+
script-agnostic, and `--locale` accepts any locale code. The Arabic-specific
|
|
412
|
+
parts are the plural rules and the bidi rules.
|
|
413
|
+
|
|
414
|
+
**Why not just use Android Studio's RTL preview?**
|
|
415
|
+
That shows you *a* layout. This fixes every layout in the app and tells you which
|
|
416
|
+
strings are still broken.
|
|
417
|
+
|
|
418
|
+
## Requirements
|
|
419
|
+
|
|
420
|
+
Python 3.9+, `apktool`, and Android SDK build-tools (`aapt2`, `zipalign`,
|
|
421
|
+
`apksigner`). Nothing else — no pip dependencies, no `lxml`, no `androguard`.
|
|
422
|
+
|
|
423
|
+
## Docs
|
|
424
|
+
|
|
425
|
+
| | |
|
|
426
|
+
|---|---|
|
|
427
|
+
| [docs/bidi.md](docs/bidi.md) | the long version of bidi: the algorithm, which marks to use and when, and what the linter can and cannot see |
|
|
428
|
+
| [docs/troubleshooting.md](docs/troubleshooting.md) | `duplicate attribute`, `minSdkVersion`, missing tools, a rebuilt app that crashes, translations that do not show up |
|
|
429
|
+
| [CHANGELOG.md](CHANGELOG.md) | release history |
|
|
430
|
+
| [SECURITY.md](SECURITY.md) | threat model, signing-key hygiene, untrusted APK input |
|
|
431
|
+
|
|
432
|
+
## Contributing
|
|
433
|
+
|
|
434
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). The test suite is plain asserts and runs
|
|
435
|
+
anywhere:
|
|
436
|
+
|
|
437
|
+
```sh
|
|
438
|
+
python tests/test_aqlab.py # 85 checks, no framework, no network, no apktool
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Most wanted: plural validation for the six Arabic forms, `android:localeConfig`
|
|
442
|
+
scaffolding for the Android 13+ per-app language picker, and vector-drawable
|
|
443
|
+
mirroring warnings. See [CHANGELOG.md](CHANGELOG.md).
|
|
444
|
+
|
|
445
|
+
## License
|
|
446
|
+
|
|
447
|
+
MIT — see [LICENSE](LICENSE).
|
|
448
|
+
|
|
449
|
+
## Disclaimer
|
|
450
|
+
|
|
451
|
+
Run it on apps you own or are permitted to modify. Redistributing a rebuilt APK
|
|
452
|
+
may infringe the original app's licence or a store's terms of service. That is
|
|
453
|
+
your call, not the tool's.
|