Membuat CatatanKu dapat diakses semua orang: keyboard navigation penuh, dukungan screen reader, kontras yang cukup, plus lokaliasi penuh dengan dukungan bahasa Indonesia dan Inggris serta format data yang benar

Di episode 10 CatatanKu tampil cantik dengan theming light/dark yang konsisten. Namun aplikasi yang indah untuk sebagian pengguna tidak otomatis dapat digunakan oleh semua pengguna. Accessibility (a11y) memastikan aplikasi bisa dipakai orang dengan berbagai kemampuan — pengguna keyboard-only, pengguna screen reader, atau mereka yang butuh kontras lebih tinggi. Localization (i18n) memastikan aplikasi bisa dipakai lintas bahasa dan budaya.
Keduanya bukan fitur "pelengkap" — keduanya adalah kualitas produk. Aksesibilitas memperluas audiens dan menghindari risiko hukum di banyak negara; lokalisasi adalah syarat memasuki pasar global. Episode ini melakukan a11y pass dan menambahkan dukungan dua bahasa (Indonesia/Inggris) untuk CatatanKu.
Pengguna desktop yang produktif jarang menyentuh mouse. Aplikasi desktop wajib punya navigasi keyboard penuh:
| Interaksi | Perilaku standar |
|---|---|
Tab / Shift+Tab | Pindah fokus antar kontrol |
Enter / Space | Aktifkan tombol/fokus |
Arrow keys | Navigasi dalam list/combo |
Esc | Tutup dialog/menu |
Ctrl+S, Ctrl+N, dll | Shortcut aplikasi |
Aturan emas: semua fungsi harus bisa dicapai keyboard, dan fokus harus selalu terlihat (focus ring). Hapus outline default hanya jika ada indikator pengganti.
// Jangan menghapus outline default. Perkuat saja:
button:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}const { globalShortcut } = require("electron");
app.whenReady().then(() => {
globalShortcut.register("CommandOrControl+Shift+Space", () => {
mainWindow.show();
mainWindow.focus();
});
});Urutan fokus harus mengikuti alur baca alami: toolbar → daftar catatan → editor → status bar. Di Flutter, urutan fokus ditentukan oleh urutan widget; di web, oleh urutan DOM. Gunakan tabIndex untuk kontrol non-interaktif yang perlu fokus — jangan memakai tabIndex acak yang membuat keyboard berjalan zig-zag.
Screen reader (JAWS, NVDA, VoiceOver, Orca) membaca pohon semantik aplikasi, bukan visualnya. Label yang jelas adalah segalanya:
<button aria-label="Tambah catatan baru">
<svg>...</svg>
</button>
<input
aria-label="Cari catatan"
placeholder="Cari catatan..."
/>
<div role="status" aria-live="polite">
Sinkronisasi selesai
</div>aria-live="polite" membuat pembaruan progres sinkronisasi diumumkan tanpa mengganggu konteks baca — pola yang kita pakai untuk semua event background (episode 9).
IconButton(
onPressed: addNote,
icon: const Icon(Icons.add),
tooltip: 'Tambah catatan baru',
// Flutter membaca tooltip + icon sebagai label otomatis
)
Semantics(
liveRegion: true,
label: 'Sinkronisasi selesai',
child: Text(syncStatus),
)Tidak ada pengganti untuk menguji dengan screen reader sungguhan:
1. Navigasi penuh dengan Tab/Shift+Tab — fokus selalu terlihat
2. Semua tombol punya label yang dibaca screen reader
3. Dialog membuka → fokus masuk; tutup → fokus kembali ke pemicu
4. Kontras teks ≥ 4.5:1; teks besar ≥ 3:1
5. Progres/status diumumkan (aria-live / liveRegion)
6. Ukuran font bisa diperbesar tanpa memotong kontenPrinsip pertama i18n: jangan pernah hardcode string UI. Semua teks dipindahkan ke resource file, lalu direferensikan dengan key:
{
"app.title": "CatatanKu",
"common.search": "Cari catatan...",
"common.add": "Tambah",
"note.untitled": "Tanpa judul",
"sync.in_progress": "Menyinkronkan...",
"sync.done": "Sinkronisasi selesai"
}{
"app.title": "CatatanKu",
"common.search": "Search notes...",
"common.add": "Add",
"note.untitled": "Untitled",
"sync.in_progress": "Syncing...",
"sync.done": "Sync complete"
}Untuk Tauri/Electron (web stack) gunakan i18next + react-i18next; untuk Flutter pakai flutter_localizations + intl. Pola pemakaiannya:
import { useTranslation } from "react-i18next";
function SearchBar() {
const { t } = useTranslation();
return <input placeholder={t("common.search")} aria-label={t("common.search")} />;
}import 'package:flutter_gen/gen_l10n/app_localizations.dart';
final l10n = AppLocalizations.of(context)!;
TextField(
decoration: InputDecoration(
hintText: l10n.commonSearch,
labelText: l10n.commonSearch,
),
);Lokalisasi yang benar mencakup tiga hal di luar teks:
// Indonesia: 17 Agustus 2026, 1.000,50
// Inggris (US): August 17, 2026, 1,000.50
final formatted = DateFormat.yMMMMd(locale).format(note.updatedAt);dd/mm/yyyy.Warning
Kesalahan klasik: menerjemahkan teks tetapi lupa memformat tanggal, angka, dan plural. Aplikasi "setengah terjemah" terasa lebih buruk daripada tidak diterjemahkan sama sekali. Hitung biaya penuh i18n — termasuk tanggal, mata uang, dan tata letak teks panjang (bahasa Jerman bisa 30% lebih panjang dari Indonesia) — sebelum memulai.
Checklist yang kita terapkan:
Ctrl+N/Ctrl+S terdaftar.aria-live/liveRegion untuk status sinkronisasi.locales/{id,en}.json, bahasa diikuti dari preferensi sistem dengan override manual di pengaturan.Inti yang harus dibawa pulang:
aria-live wajib.Di episode 12 selanjutnya kita masuk fase produksi: packaging & distribution — installer WiX/NSIS/DMG/DEB, code signing, dan strategi distribusi aplikasi desktop yang siap dikirim ke pengguna. Sampai jumpa di episode 12!