qualia-framework 2.1.0
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.
- package/README.md +50 -0
- package/bin/cli.js +519 -0
- package/framework/agents/architecture-strategist.md +53 -0
- package/framework/agents/backend-agent.md +150 -0
- package/framework/agents/code-simplicity-reviewer.md +86 -0
- package/framework/agents/frontend-agent.md +111 -0
- package/framework/agents/kieran-typescript-reviewer.md +96 -0
- package/framework/agents/performance-oracle.md +111 -0
- package/framework/agents/qualia-codebase-mapper.md +760 -0
- package/framework/agents/qualia-debugger.md +1203 -0
- package/framework/agents/qualia-executor.md +881 -0
- package/framework/agents/qualia-integration-checker.md +423 -0
- package/framework/agents/qualia-phase-researcher.md +453 -0
- package/framework/agents/qualia-plan-checker.md +699 -0
- package/framework/agents/qualia-planner.md +1241 -0
- package/framework/agents/qualia-project-researcher.md +602 -0
- package/framework/agents/qualia-research-synthesizer.md +236 -0
- package/framework/agents/qualia-roadmapper.md +605 -0
- package/framework/agents/qualia-verifier.md +685 -0
- package/framework/agents/team-orchestrator.md +228 -0
- package/framework/agents/teams/full-stack-team.md +48 -0
- package/framework/agents/teams/optimize-team.md +53 -0
- package/framework/agents/teams/review-team.md +62 -0
- package/framework/agents/teams/ship-team.md +86 -0
- package/framework/agents/test-agent.md +182 -0
- package/framework/askpass.sh +2 -0
- package/framework/commands/design.md +53 -0
- package/framework/commands/quick-db.md +22 -0
- package/framework/config/retention.json +35 -0
- package/framework/core/PRINCIPLES.md +77 -0
- package/framework/hooks/auto-format.sh +45 -0
- package/framework/hooks/block-env-edit.sh +42 -0
- package/framework/hooks/branch-guard.sh +46 -0
- package/framework/hooks/confirm-delete.sh +56 -0
- package/framework/hooks/migration-validate.sh +68 -0
- package/framework/hooks/notification-speak.sh +15 -0
- package/framework/hooks/pre-commit.sh +80 -0
- package/framework/hooks/pre-compact.sh +55 -0
- package/framework/hooks/pre-deploy-gate.sh +151 -0
- package/framework/hooks/qualia-colors.sh +32 -0
- package/framework/hooks/retention-cleanup.sh +43 -0
- package/framework/hooks/save-session-state.sh +153 -0
- package/framework/hooks/session-context-loader.sh +28 -0
- package/framework/hooks/session-learn.sh +30 -0
- package/framework/knowledge/claudecode-bible.md +1384 -0
- package/framework/knowledge/client-prefs.md +22 -0
- package/framework/knowledge/common-fixes.md +25 -0
- package/framework/knowledge/deployment-map.md +35 -0
- package/framework/knowledge/email-signature.html +1 -0
- package/framework/knowledge/employees.md +8 -0
- package/framework/knowledge/learned-patterns.md +51 -0
- package/framework/knowledge/optimization-research-2026.md +137 -0
- package/framework/knowledge/qualia-context.md +67 -0
- package/framework/knowledge/supabase-patterns.md +50 -0
- package/framework/knowledge/voice-agent-patterns.md +46 -0
- package/framework/qualia-engine/VERSION +1 -0
- package/framework/qualia-engine/bin/qualia-tools.js +2160 -0
- package/framework/qualia-engine/bin/qualia-tools.test.js +1054 -0
- package/framework/qualia-engine/references/checkpoints.md +775 -0
- package/framework/qualia-engine/references/continuation-format.md +249 -0
- package/framework/qualia-engine/references/decimal-phase-calculation.md +65 -0
- package/framework/qualia-engine/references/design-quality.md +56 -0
- package/framework/qualia-engine/references/git-integration.md +254 -0
- package/framework/qualia-engine/references/git-planning-commit.md +50 -0
- package/framework/qualia-engine/references/model-profile-resolution.md +32 -0
- package/framework/qualia-engine/references/model-profiles.md +73 -0
- package/framework/qualia-engine/references/phase-argument-parsing.md +61 -0
- package/framework/qualia-engine/references/planning-config.md +195 -0
- package/framework/qualia-engine/references/questioning.md +141 -0
- package/framework/qualia-engine/references/tdd.md +263 -0
- package/framework/qualia-engine/references/ui-brand.md +160 -0
- package/framework/qualia-engine/references/verification-patterns.md +612 -0
- package/framework/qualia-engine/templates/DEBUG.md +159 -0
- package/framework/qualia-engine/templates/DESIGN.md +81 -0
- package/framework/qualia-engine/templates/UAT.md +247 -0
- package/framework/qualia-engine/templates/codebase/architecture.md +255 -0
- package/framework/qualia-engine/templates/codebase/concerns.md +310 -0
- package/framework/qualia-engine/templates/codebase/conventions.md +307 -0
- package/framework/qualia-engine/templates/codebase/integrations.md +280 -0
- package/framework/qualia-engine/templates/codebase/stack.md +186 -0
- package/framework/qualia-engine/templates/codebase/structure.md +285 -0
- package/framework/qualia-engine/templates/codebase/testing.md +480 -0
- package/framework/qualia-engine/templates/config.json +35 -0
- package/framework/qualia-engine/templates/context.md +283 -0
- package/framework/qualia-engine/templates/continue-here.md +78 -0
- package/framework/qualia-engine/templates/debug-subagent-prompt.md +91 -0
- package/framework/qualia-engine/templates/discovery.md +146 -0
- package/framework/qualia-engine/templates/milestone-archive.md +123 -0
- package/framework/qualia-engine/templates/milestone.md +115 -0
- package/framework/qualia-engine/templates/phase-prompt.md +567 -0
- package/framework/qualia-engine/templates/planner-subagent-prompt.md +117 -0
- package/framework/qualia-engine/templates/project.md +184 -0
- package/framework/qualia-engine/templates/projects/ai-agent.md +156 -0
- package/framework/qualia-engine/templates/projects/mobile-app.md +181 -0
- package/framework/qualia-engine/templates/projects/voice-agent.md +134 -0
- package/framework/qualia-engine/templates/projects/website.md +137 -0
- package/framework/qualia-engine/templates/requirements.md +231 -0
- package/framework/qualia-engine/templates/research-project/ARCHITECTURE.md +204 -0
- package/framework/qualia-engine/templates/research-project/FEATURES.md +147 -0
- package/framework/qualia-engine/templates/research-project/PITFALLS.md +200 -0
- package/framework/qualia-engine/templates/research-project/STACK.md +120 -0
- package/framework/qualia-engine/templates/research-project/SUMMARY.md +170 -0
- package/framework/qualia-engine/templates/research.md +552 -0
- package/framework/qualia-engine/templates/roadmap.md +202 -0
- package/framework/qualia-engine/templates/state.md +176 -0
- package/framework/qualia-engine/templates/summary-complex.md +59 -0
- package/framework/qualia-engine/templates/summary-minimal.md +41 -0
- package/framework/qualia-engine/templates/summary-standard.md +48 -0
- package/framework/qualia-engine/templates/summary.md +246 -0
- package/framework/qualia-engine/templates/user-setup.md +311 -0
- package/framework/qualia-engine/templates/verification-report.md +322 -0
- package/framework/qualia-engine/workflows/add-phase.md +179 -0
- package/framework/qualia-engine/workflows/add-todo.md +157 -0
- package/framework/qualia-engine/workflows/audit-milestone.md +241 -0
- package/framework/qualia-engine/workflows/check-todos.md +176 -0
- package/framework/qualia-engine/workflows/complete-milestone.md +858 -0
- package/framework/qualia-engine/workflows/diagnose-issues.md +219 -0
- package/framework/qualia-engine/workflows/discovery-phase.md +289 -0
- package/framework/qualia-engine/workflows/discuss-phase.md +534 -0
- package/framework/qualia-engine/workflows/execute-phase.md +559 -0
- package/framework/qualia-engine/workflows/execute-plan.md +438 -0
- package/framework/qualia-engine/workflows/help.md +470 -0
- package/framework/qualia-engine/workflows/insert-phase.md +220 -0
- package/framework/qualia-engine/workflows/list-phase-assumptions.md +178 -0
- package/framework/qualia-engine/workflows/map-codebase.md +327 -0
- package/framework/qualia-engine/workflows/new-milestone.md +363 -0
- package/framework/qualia-engine/workflows/new-project.md +1037 -0
- package/framework/qualia-engine/workflows/pause-work.md +122 -0
- package/framework/qualia-engine/workflows/plan-milestone-gaps.md +256 -0
- package/framework/qualia-engine/workflows/plan-phase.md +422 -0
- package/framework/qualia-engine/workflows/progress.md +354 -0
- package/framework/qualia-engine/workflows/quick.md +252 -0
- package/framework/qualia-engine/workflows/remove-phase.md +326 -0
- package/framework/qualia-engine/workflows/research-phase.md +74 -0
- package/framework/qualia-engine/workflows/resume-project.md +306 -0
- package/framework/qualia-engine/workflows/set-profile.md +80 -0
- package/framework/qualia-engine/workflows/settings.md +145 -0
- package/framework/qualia-engine/workflows/transition.md +556 -0
- package/framework/qualia-engine/workflows/update.md +197 -0
- package/framework/qualia-engine/workflows/verify-phase.md +195 -0
- package/framework/qualia-engine/workflows/verify-work.md +625 -0
- package/framework/rules/context7.md +11 -0
- package/framework/rules/deployment.md +29 -0
- package/framework/rules/frontend.md +33 -0
- package/framework/rules/security.md +12 -0
- package/framework/rules/speed.md +20 -0
- package/framework/scripts/__pycache__/say.cpython-314.pyc +0 -0
- package/framework/scripts/apply-retention.sh +120 -0
- package/framework/scripts/bootstrap-pop-os.sh +354 -0
- package/framework/scripts/claude-voice +13 -0
- package/framework/scripts/cleanup.sh +131 -0
- package/framework/scripts/cowork-mode.sh +141 -0
- package/framework/scripts/generate-project-claude-md.sh +153 -0
- package/framework/scripts/load-test-webhook.js +172 -0
- package/framework/scripts/say.py +236 -0
- package/framework/scripts/showcase-video-recorder/ffmpeg-builder.js +167 -0
- package/framework/scripts/showcase-video-recorder/playwright-helpers.js +216 -0
- package/framework/scripts/speak.py +55 -0
- package/framework/scripts/speak.sh +18 -0
- package/framework/scripts/status.sh +138 -0
- package/framework/scripts/sync-to-framework.sh +65 -0
- package/framework/scripts/voice-hotkey.py +227 -0
- package/framework/scripts/voice-input.sh +51 -0
- package/framework/skills/animate/SKILL.md +202 -0
- package/framework/skills/bolder/SKILL.md +144 -0
- package/framework/skills/browser-qa/SKILL.md +536 -0
- package/framework/skills/clarify/SKILL.md +179 -0
- package/framework/skills/colorize/SKILL.md +170 -0
- package/framework/skills/critique/SKILL.md +126 -0
- package/framework/skills/deep-research/SKILL.md +271 -0
- package/framework/skills/delight/SKILL.md +329 -0
- package/framework/skills/deploy/SKILL.md +261 -0
- package/framework/skills/deploy-verify/SKILL.md +377 -0
- package/framework/skills/deploy-verify/scripts/canary-check.sh +206 -0
- package/framework/skills/deploy-verify/scripts/check-console-errors.js +147 -0
- package/framework/skills/deploy-verify/scripts/check-cwv.js +139 -0
- package/framework/skills/deploy-verify/scripts/project-detect.sh +84 -0
- package/framework/skills/deploy-verify/scripts/verify.sh +548 -0
- package/framework/skills/design-quieter/SKILL.md +130 -0
- package/framework/skills/distill/SKILL.md +149 -0
- package/framework/skills/docs-lookup/SKILL.md +78 -0
- package/framework/skills/fcm-notifications/SKILL.md +125 -0
- package/framework/skills/financial-ledger/SKILL.md +1039 -0
- package/framework/skills/frontend-master/NOTICE.md +4 -0
- package/framework/skills/frontend-master/SKILL.md +127 -0
- package/framework/skills/frontend-master/reference/color-and-contrast.md +132 -0
- package/framework/skills/frontend-master/reference/interaction-design.md +123 -0
- package/framework/skills/frontend-master/reference/motion-design.md +99 -0
- package/framework/skills/frontend-master/reference/responsive-design.md +114 -0
- package/framework/skills/frontend-master/reference/spatial-design.md +100 -0
- package/framework/skills/frontend-master/reference/typography.md +131 -0
- package/framework/skills/frontend-master/reference/ux-writing.md +107 -0
- package/framework/skills/harden/SKILL.md +357 -0
- package/framework/skills/i18n-rtl/SKILL.md +752 -0
- package/framework/skills/learn/SKILL.md +71 -0
- package/framework/skills/memory/SKILL.md +50 -0
- package/framework/skills/mobile-expo/SKILL.md +864 -0
- package/framework/skills/mobile-expo/references/store-checklist.md +550 -0
- package/framework/skills/nestjs-backend/README.md +73 -0
- package/framework/skills/nestjs-backend/SKILL.md +446 -0
- package/framework/skills/nestjs-backend/references/templates.md +1173 -0
- package/framework/skills/normalize/SKILL.md +79 -0
- package/framework/skills/onboard/SKILL.md +242 -0
- package/framework/skills/polish/SKILL.md +209 -0
- package/framework/skills/pr/SKILL.md +66 -0
- package/framework/skills/qualia/SKILL.md +153 -0
- package/framework/skills/qualia-add-todo/SKILL.md +68 -0
- package/framework/skills/qualia-audit-milestone/SKILL.md +92 -0
- package/framework/skills/qualia-check-todos/SKILL.md +55 -0
- package/framework/skills/qualia-complete-milestone/SKILL.md +108 -0
- package/framework/skills/qualia-debug/SKILL.md +149 -0
- package/framework/skills/qualia-design/SKILL.md +203 -0
- package/framework/skills/qualia-discuss-phase/SKILL.md +72 -0
- package/framework/skills/qualia-execute-phase/SKILL.md +86 -0
- package/framework/skills/qualia-help/SKILL.md +67 -0
- package/framework/skills/qualia-idk/SKILL.md +352 -0
- package/framework/skills/qualia-list-phase-assumptions/SKILL.md +67 -0
- package/framework/skills/qualia-new-milestone/SKILL.md +72 -0
- package/framework/skills/qualia-new-project/SKILL.md +92 -0
- package/framework/skills/qualia-optimize/SKILL.md +417 -0
- package/framework/skills/qualia-pause-work/SKILL.md +96 -0
- package/framework/skills/qualia-plan-milestone-gaps/SKILL.md +57 -0
- package/framework/skills/qualia-plan-phase/SKILL.md +101 -0
- package/framework/skills/qualia-progress/SKILL.md +53 -0
- package/framework/skills/qualia-quick/SKILL.md +89 -0
- package/framework/skills/qualia-research-phase/SKILL.md +88 -0
- package/framework/skills/qualia-resume-work/SKILL.md +62 -0
- package/framework/skills/qualia-review/SKILL.md +263 -0
- package/framework/skills/qualia-start/SKILL.md +182 -0
- package/framework/skills/qualia-verify-work/SKILL.md +105 -0
- package/framework/skills/qualia-workflow/SKILL.md +130 -0
- package/framework/skills/rag/SKILL.md +750 -0
- package/framework/skills/responsive/SKILL.md +231 -0
- package/framework/skills/retro/SKILL.md +284 -0
- package/framework/skills/sakani-conventions/SKILL.md +136 -0
- package/framework/skills/sakani-conventions/evals/evals.json +23 -0
- package/framework/skills/sakani-conventions/references/entities.md +365 -0
- package/framework/skills/sakani-conventions/references/error-codes.md +95 -0
- package/framework/skills/seo-master/SKILL.md +490 -0
- package/framework/skills/seo-master/references/checklist.md +199 -0
- package/framework/skills/seo-master/references/structured-data.md +609 -0
- package/framework/skills/ship/SKILL.md +202 -0
- package/framework/skills/stack-researcher/SKILL.md +215 -0
- package/framework/skills/status/SKILL.md +154 -0
- package/framework/skills/status/scripts/health-check.sh +562 -0
- package/framework/skills/subscription-payments/SKILL.md +250 -0
- package/framework/skills/supabase/SKILL.md +973 -0
- package/framework/skills/supabase/references/templates.md +159 -0
- package/framework/skills/team/SKILL.md +67 -0
- package/framework/skills/test-runner/SKILL.md +202 -0
- package/framework/skills/voice-agent/SKILL.md +407 -0
- package/framework/skills/zoho-workflow/SKILL.md +51 -0
- package/framework/statusline-command.sh +117 -0
- package/package.json +24 -0
- package/profiles/fawzi.json +16 -0
- package/profiles/hasan.json +16 -0
- package/profiles/moayad.json +16 -0
- package/templates/CLAUDE-owner.md +52 -0
- package/templates/CLAUDE.md.hbs +58 -0
- package/templates/env.claude.template +12 -0
- package/templates/settings.json +141 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: i18n-rtl
|
|
3
|
+
description: "Internationalization and RTL (Right-to-Left) layout patterns — Arabic/English bilingual apps, RTL CSS/Tailwind, message key systems, locale switching, numeral handling, date/time formatting, and bidirectional text. Use whenever implementing multi-language support, Arabic UI, RTL layouts, translation systems, or locale-aware formatting in web or mobile apps. Triggers on: i18n, internationalization, RTL, right-to-left, Arabic, translation, locale, language switching, bilingual, message keys, متعدد اللغات."
|
|
4
|
+
tags: [i18n, rtl, arabic, localization]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Internationalization & RTL (Right-to-Left) Layout Patterns
|
|
8
|
+
|
|
9
|
+
This skill covers Arabic/English bilingual application design—message key architectures, RTL layout strategies, locale detection, numeral display, date/time formatting, and bidirectional text handling.
|
|
10
|
+
|
|
11
|
+
## Core Architecture
|
|
12
|
+
|
|
13
|
+
### Message Key System
|
|
14
|
+
|
|
15
|
+
All user-facing strings must use message keys. No hardcoded text anywhere—this enables translation, consistency, and independent text updates without code changes.
|
|
16
|
+
|
|
17
|
+
**Backend Contract:**
|
|
18
|
+
- Backend returns only message keys and structured data
|
|
19
|
+
- Never return display text from the server
|
|
20
|
+
- Include all context needed for plural/conditional rendering
|
|
21
|
+
|
|
22
|
+
**Frontend Resolution:**
|
|
23
|
+
- React context or hooks resolve keys → localized strings
|
|
24
|
+
- Fallback chain: user locale → default locale (ar) → key itself
|
|
25
|
+
|
|
26
|
+
**Key Naming Convention:**
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// In @sakani/shared or dedicated i18n package
|
|
30
|
+
// Dot-notation, grouped by domain and entity
|
|
31
|
+
const translations = {
|
|
32
|
+
ar: {
|
|
33
|
+
// Building domain
|
|
34
|
+
'building.name': 'اسم المبنى',
|
|
35
|
+
'building.type.residential': 'سكني',
|
|
36
|
+
'building.type.commercial': 'تجاري',
|
|
37
|
+
'building.label.floorNumber': 'رقم الطابق',
|
|
38
|
+
|
|
39
|
+
// Error domain (all error codes → message keys)
|
|
40
|
+
'error.auth.unauthenticated': 'يرجى تسجيل الدخول',
|
|
41
|
+
'error.auth.forbidden': 'ليس لديك صلاحيات كافية',
|
|
42
|
+
'error.validation.required': 'هذا الحقل مطلوب',
|
|
43
|
+
'error.validation.emailInvalid': 'البريد الإلكتروني غير صحيح',
|
|
44
|
+
|
|
45
|
+
// Dues domain (reason codes → message keys)
|
|
46
|
+
'dues.status.paid': 'مدفوع',
|
|
47
|
+
'dues.status.unpaid': 'غير مدفوع',
|
|
48
|
+
'dues.status.late': 'متأخر',
|
|
49
|
+
'dues.reason.regularExpense': 'مصروف منتظم',
|
|
50
|
+
'dues.reason.specialAssessment': 'تقييم خاص',
|
|
51
|
+
|
|
52
|
+
// Action domain
|
|
53
|
+
'action.create': 'إنشاء',
|
|
54
|
+
'action.update': 'تحديث',
|
|
55
|
+
'action.delete': 'حذف',
|
|
56
|
+
'action.save': 'حفظ',
|
|
57
|
+
'action.cancel': 'إلغاء',
|
|
58
|
+
},
|
|
59
|
+
en: {
|
|
60
|
+
'building.name': 'Building Name',
|
|
61
|
+
'building.type.residential': 'Residential',
|
|
62
|
+
'building.type.commercial': 'Commercial',
|
|
63
|
+
'building.label.floorNumber': 'Floor Number',
|
|
64
|
+
|
|
65
|
+
'error.auth.unauthenticated': 'Please sign in',
|
|
66
|
+
'error.auth.forbidden': 'You do not have permission',
|
|
67
|
+
'error.validation.required': 'This field is required',
|
|
68
|
+
'error.validation.emailInvalid': 'Email address is invalid',
|
|
69
|
+
|
|
70
|
+
'dues.status.paid': 'Paid',
|
|
71
|
+
'dues.status.unpaid': 'Unpaid',
|
|
72
|
+
'dues.status.late': 'Late',
|
|
73
|
+
'dues.reason.regularExpense': 'Regular Expense',
|
|
74
|
+
'dues.reason.specialAssessment': 'Special Assessment',
|
|
75
|
+
|
|
76
|
+
'action.create': 'Create',
|
|
77
|
+
'action.update': 'Update',
|
|
78
|
+
'action.delete': 'Delete',
|
|
79
|
+
'action.save': 'Save',
|
|
80
|
+
'action.cancel': 'Cancel',
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Naming Pattern:**
|
|
86
|
+
- `{domain}.{entity}.{property}` — for attributes/labels
|
|
87
|
+
- `{domain}.{action}.{context}` — for operations/statuses
|
|
88
|
+
- All error codes map 1:1 to a message key
|
|
89
|
+
- All reason/status codes are message keys, not magic strings
|
|
90
|
+
|
|
91
|
+
### Locale Detection & Persistence
|
|
92
|
+
|
|
93
|
+
**Priority Order:**
|
|
94
|
+
1. User preference (profile setting, synced across devices)
|
|
95
|
+
2. Device locale (from OS settings)
|
|
96
|
+
3. Default: `ar` (Arabic)
|
|
97
|
+
|
|
98
|
+
**Persistence:**
|
|
99
|
+
- Web: localStorage + server user profile
|
|
100
|
+
- Mobile: AsyncStorage + server user profile
|
|
101
|
+
- On app load: fetch user preference from profile (source of truth)
|
|
102
|
+
- On logout: fall back to device locale
|
|
103
|
+
- Offline: use last known preference from device storage
|
|
104
|
+
|
|
105
|
+
## RTL Layout — Web (Tailwind CSS)
|
|
106
|
+
|
|
107
|
+
### Setup
|
|
108
|
+
|
|
109
|
+
Install and configure `tailwindcss-rtl` plugin:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm install tailwindcss-rtl
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
// tailwind.config.js
|
|
117
|
+
module.exports = {
|
|
118
|
+
plugins: [require('tailwindcss-rtl')],
|
|
119
|
+
};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Logical Properties (Not Physical)
|
|
123
|
+
|
|
124
|
+
Use Tailwind's logical property system—`start`/`end` instead of `left`/`right`, which automatically flip based on text direction.
|
|
125
|
+
|
|
126
|
+
**Examples:**
|
|
127
|
+
|
|
128
|
+
| Use | Don't Use | When RTL |
|
|
129
|
+
|-----|-----------|---------|
|
|
130
|
+
| `ms-4` | `ml-4` | becomes `margin-right` |
|
|
131
|
+
| `ps-8` | `pl-8` | becomes `padding-right` |
|
|
132
|
+
| `text-start` | `text-left` | becomes `text-right` |
|
|
133
|
+
| `float-start` | `float-left` | becomes `float-right` |
|
|
134
|
+
| `rounded-s-lg` | `rounded-l-lg` | becomes `rounded-r-lg` |
|
|
135
|
+
| `border-s-2` | `border-l-2` | becomes `border-r-2` |
|
|
136
|
+
| `inset-s-0` | `inset-l-0` | becomes `inset-r-0` |
|
|
137
|
+
|
|
138
|
+
**Complete Example:**
|
|
139
|
+
|
|
140
|
+
```jsx
|
|
141
|
+
// Header with logo (start) and menu button (end)
|
|
142
|
+
<header className="flex items-center justify-between p-4">
|
|
143
|
+
<div className="w-10 h-10">Logo</div>
|
|
144
|
+
<button>Menu</button>
|
|
145
|
+
</header>
|
|
146
|
+
|
|
147
|
+
// Sidebar layout — flex-col on mobile, row on desktop
|
|
148
|
+
// RTL: flex-row-reverse flips the sidebar to the right
|
|
149
|
+
<div className="flex flex-col md:flex-row md:rtl:flex-row-reverse gap-4">
|
|
150
|
+
<aside className="w-full md:w-64">Sidebar</aside>
|
|
151
|
+
<main className="flex-1">Content</main>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
// List with icon (left in LTR, right in RTL)
|
|
155
|
+
<li className="flex items-center gap-3">
|
|
156
|
+
<span className="text-xl">✓</span>
|
|
157
|
+
<span>Item text</span>
|
|
158
|
+
</li>
|
|
159
|
+
|
|
160
|
+
// Input with icon inside
|
|
161
|
+
<div className="relative">
|
|
162
|
+
<input className="ps-10 pe-4" type="text" />
|
|
163
|
+
<span className="absolute inset-s-0 flex items-center ps-3">🔍</span>
|
|
164
|
+
</div>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Root Element & Direction
|
|
168
|
+
|
|
169
|
+
Set `dir` attribute on the root element:
|
|
170
|
+
|
|
171
|
+
```jsx
|
|
172
|
+
// In your layout wrapper or _app.tsx
|
|
173
|
+
import { useLocale } from '@/hooks/useLocale';
|
|
174
|
+
|
|
175
|
+
export default function App({ Component, pageProps }) {
|
|
176
|
+
const { locale } = useLocale();
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<html dir={locale === 'ar' ? 'rtl' : 'ltr'}>
|
|
180
|
+
<head>
|
|
181
|
+
<meta charSet="utf-8" />
|
|
182
|
+
</head>
|
|
183
|
+
<body>
|
|
184
|
+
<Component {...pageProps} />
|
|
185
|
+
</body>
|
|
186
|
+
</html>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Or with Next.js `next-intl`:
|
|
192
|
+
|
|
193
|
+
```jsx
|
|
194
|
+
import { useLocale } from 'next-intl';
|
|
195
|
+
|
|
196
|
+
export default function RootLayout({ children }) {
|
|
197
|
+
const locale = useLocale();
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<html dir={locale === 'ar' ? 'rtl' : 'ltr'} lang={locale}>
|
|
201
|
+
{children}
|
|
202
|
+
</html>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### RTL-Specific Overrides
|
|
208
|
+
|
|
209
|
+
Use the `rtl:` variant for RTL-only styles:
|
|
210
|
+
|
|
211
|
+
```jsx
|
|
212
|
+
// Icon that should point right in LTR, left in RTL
|
|
213
|
+
<svg className="w-4 h-4 rtl:rotate-180">
|
|
214
|
+
<use href="#arrow-right" />
|
|
215
|
+
</svg>
|
|
216
|
+
|
|
217
|
+
// Navigation arrow that reverses in RTL
|
|
218
|
+
<button className="flex items-center gap-2 rtl:flex-row-reverse">
|
|
219
|
+
<span>Next</span>
|
|
220
|
+
<ChevronRight className="w-4 h-4" />
|
|
221
|
+
</button>
|
|
222
|
+
|
|
223
|
+
// Grid that becomes single column in RTL (depends on layout)
|
|
224
|
+
<div className="grid grid-cols-3 gap-4 rtl:grid-cols-1">
|
|
225
|
+
...
|
|
226
|
+
</div>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Icons & Graphics
|
|
230
|
+
|
|
231
|
+
Icons need manual flipping in RTL. Common ones to flip:
|
|
232
|
+
|
|
233
|
+
- Chevrons / arrows (right, left)
|
|
234
|
+
- Play buttons (play, pause, skip)
|
|
235
|
+
- Notifications (bell, envelope)
|
|
236
|
+
- Checkmarks / close icons (usually symmetric, no flip)
|
|
237
|
+
|
|
238
|
+
**Implementation:**
|
|
239
|
+
|
|
240
|
+
```jsx
|
|
241
|
+
import { useLocale } from '@/hooks/useLocale';
|
|
242
|
+
|
|
243
|
+
function ChevronRight() {
|
|
244
|
+
const { locale } = useLocale();
|
|
245
|
+
const isRTL = locale === 'ar';
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<svg className={isRTL ? 'rotate-180' : ''}>
|
|
249
|
+
<path d="M9 5l5 7-5 7" />
|
|
250
|
+
</svg>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Or use a utility:
|
|
255
|
+
function DirectionalIcon({ children, icon }) {
|
|
256
|
+
const { locale } = useLocale();
|
|
257
|
+
return locale === 'ar' && icon.flipsInRTL ? (
|
|
258
|
+
<span className="inline-block rotate-180">{children}</span>
|
|
259
|
+
) : (
|
|
260
|
+
children
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## RTL Layout — React Native
|
|
266
|
+
|
|
267
|
+
### Forced RTL Mode
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { I18nManager } from 'react-native';
|
|
271
|
+
import { useLocale } from '@/hooks/useLocale';
|
|
272
|
+
|
|
273
|
+
// In app initialization / locale context
|
|
274
|
+
export function useRTL() {
|
|
275
|
+
const { locale } = useLocale();
|
|
276
|
+
const isRTL = locale === 'ar';
|
|
277
|
+
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
I18nManager.forceRTL(isRTL);
|
|
280
|
+
// Note: May require app reload on some platforms
|
|
281
|
+
}, [isRTL]);
|
|
282
|
+
|
|
283
|
+
return isRTL;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check current RTL state
|
|
287
|
+
const isCurrentlyRTL = I18nManager.isRTL;
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Layout Patterns
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { StyleSheet, View, Text, I18nManager } from 'react-native';
|
|
294
|
+
|
|
295
|
+
const styles = StyleSheet.create({
|
|
296
|
+
// Use logical properties where possible
|
|
297
|
+
container: {
|
|
298
|
+
paddingStart: 16,
|
|
299
|
+
paddingEnd: 16,
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
// Flip flex direction for RTL
|
|
303
|
+
row: {
|
|
304
|
+
flexDirection: 'row',
|
|
305
|
+
},
|
|
306
|
+
rowReverse: {
|
|
307
|
+
flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
// Text alignment
|
|
311
|
+
textStart: {
|
|
312
|
+
textAlign: I18nManager.isRTL ? 'right' : 'left',
|
|
313
|
+
},
|
|
314
|
+
textEnd: {
|
|
315
|
+
textAlign: I18nManager.isRTL ? 'left' : 'right',
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Component with RTL awareness
|
|
320
|
+
function Header() {
|
|
321
|
+
const isRTL = I18nManager.isRTL;
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<View style={[styles.row, { justifyContent: 'space-between' }]}>
|
|
325
|
+
<Text>Title</Text>
|
|
326
|
+
<Icon
|
|
327
|
+
name={isRTL ? 'chevron-left' : 'chevron-right'}
|
|
328
|
+
size={24}
|
|
329
|
+
/>
|
|
330
|
+
</View>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### TextInput & Text Direction
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
<TextInput
|
|
339
|
+
style={{ textAlign: I18nManager.isRTL ? 'right' : 'left' }}
|
|
340
|
+
writingDirection={I18nManager.isRTL ? 'rtl' : 'ltr'}
|
|
341
|
+
placeholder="Type here..."
|
|
342
|
+
/>
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Numeral Display
|
|
346
|
+
|
|
347
|
+
**Rule: Always display Western Arabic numerals (0–9), never Eastern Arabic (٠–٩).**
|
|
348
|
+
|
|
349
|
+
Even in Arabic locale, use `0-9`. This applies to:
|
|
350
|
+
- Prices: `1,234.56`
|
|
351
|
+
- Quantities: `12`
|
|
352
|
+
- Dates: `2025-03-06`
|
|
353
|
+
- Phone numbers: `+962 6 123 4567`
|
|
354
|
+
|
|
355
|
+
**Implementation:**
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// Correct: Western numerals in Arabic
|
|
359
|
+
const formatter = new Intl.NumberFormat('ar-JO');
|
|
360
|
+
formatter.format(1234); // → "1,234"
|
|
361
|
+
|
|
362
|
+
// Correct: Using date-fns (automatically handles numerals)
|
|
363
|
+
import { format } from 'date-fns';
|
|
364
|
+
import { ar } from 'date-fns/locale';
|
|
365
|
+
format(new Date(), 'd/M/yyyy', { locale: ar }); // → "06/03/2025"
|
|
366
|
+
|
|
367
|
+
// Avoid: Eastern Arabic numerals
|
|
368
|
+
// ❌ "١٢٣٤" — don't display this in UI
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## Date & Time Formatting
|
|
372
|
+
|
|
373
|
+
### Storage & Timezone
|
|
374
|
+
|
|
375
|
+
- **Store:** Always UTC in the database
|
|
376
|
+
- **Display:** Asia/Amman timezone (Jordan local time)
|
|
377
|
+
- **Format:** Use `Intl.DateTimeFormat` or `date-fns` with locale
|
|
378
|
+
|
|
379
|
+
### Implementation
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
import { format } from 'date-fns';
|
|
383
|
+
import { ar, en } from 'date-fns/locale';
|
|
384
|
+
|
|
385
|
+
function formatDate(isoDate: string, locale: 'ar' | 'en') {
|
|
386
|
+
const date = new Date(isoDate);
|
|
387
|
+
|
|
388
|
+
if (locale === 'ar') {
|
|
389
|
+
return format(date, 'd MMMM yyyy', { locale: ar });
|
|
390
|
+
// → "06 مارس 2025"
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return format(date, 'd MMMM yyyy', { locale: en });
|
|
394
|
+
// → "6 March 2025"
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Or with Intl.DateTimeFormat
|
|
398
|
+
function formatDateIntl(isoDate: string, locale: 'ar' | 'en') {
|
|
399
|
+
const date = new Date(isoDate);
|
|
400
|
+
return new Intl.DateTimeFormat(locale === 'ar' ? 'ar-JO' : 'en-GB', {
|
|
401
|
+
year: 'numeric',
|
|
402
|
+
month: 'long',
|
|
403
|
+
day: 'numeric',
|
|
404
|
+
timeZone: 'Asia/Amman',
|
|
405
|
+
}).format(date);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Time with timezone awareness
|
|
409
|
+
function formatTime(isoDate: string, locale: 'ar' | 'en') {
|
|
410
|
+
const date = new Date(isoDate);
|
|
411
|
+
return new Intl.DateTimeFormat(locale === 'ar' ? 'ar-JO' : 'en-GB', {
|
|
412
|
+
hour: '2-digit',
|
|
413
|
+
minute: '2-digit',
|
|
414
|
+
second: '2-digit',
|
|
415
|
+
timeZone: 'Asia/Amman',
|
|
416
|
+
}).format(date);
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Date Formats by Locale
|
|
421
|
+
|
|
422
|
+
- **Arabic:** `DD/MM/YYYY` — `06/03/2025`
|
|
423
|
+
- **English:** `DD/MM/YYYY` or `MM/DD/YYYY` depending on region
|
|
424
|
+
- Always use locale-aware formatters, don't hardcode
|
|
425
|
+
|
|
426
|
+
## Currency Formatting
|
|
427
|
+
|
|
428
|
+
### Jordan Dinar (JOD)
|
|
429
|
+
|
|
430
|
+
- **Currency Code:** JOD
|
|
431
|
+
- **Decimal Places:** 3
|
|
432
|
+
- **Decimal Separator:** `.` (universal) or `٫` (Arabic specific)
|
|
433
|
+
- **Symbol Position:** After amount in Arabic, before in English
|
|
434
|
+
|
|
435
|
+
### Implementation
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
function formatCurrency(amount: number, locale: 'ar' | 'en') {
|
|
439
|
+
const formatter = new Intl.NumberFormat(locale === 'ar' ? 'ar-JO' : 'en-GB', {
|
|
440
|
+
style: 'currency',
|
|
441
|
+
currency: 'JOD',
|
|
442
|
+
minimumFractionDigits: 3,
|
|
443
|
+
maximumFractionDigits: 3,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
return formatter.format(amount);
|
|
447
|
+
// ar: "12٫000 د.أ"
|
|
448
|
+
// en: "JOD 12.000"
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Manual formatting with custom formatting
|
|
452
|
+
function formatCurrencyCustom(amount: number, locale: 'ar' | 'en') {
|
|
453
|
+
const numStr = amount.toFixed(3);
|
|
454
|
+
const symbol = 'د.أ'; // JOD symbol
|
|
455
|
+
|
|
456
|
+
if (locale === 'ar') {
|
|
457
|
+
return `${numStr} ${symbol}`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return `JOD ${numStr}`;
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Locale Switching
|
|
465
|
+
|
|
466
|
+
### User Preference Flow
|
|
467
|
+
|
|
468
|
+
1. **On App Load:**
|
|
469
|
+
- Fetch user profile (includes locale preference)
|
|
470
|
+
- If missing, use device locale
|
|
471
|
+
- If not ar/en, default to ar
|
|
472
|
+
|
|
473
|
+
2. **User Changes Language:**
|
|
474
|
+
- Update user profile (API call)
|
|
475
|
+
- Update local context/state
|
|
476
|
+
- Update UI direction (`dir` attribute or `I18nManager.forceRTL`)
|
|
477
|
+
- Reload component tree or update i18n library
|
|
478
|
+
|
|
479
|
+
3. **Persistence:**
|
|
480
|
+
- Web: localStorage + server (server is source of truth)
|
|
481
|
+
- Mobile: AsyncStorage + server (server is source of truth)
|
|
482
|
+
|
|
483
|
+
### Implementation
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
// useLocale hook
|
|
487
|
+
import { createContext, useContext, useState, useEffect } from 'react';
|
|
488
|
+
|
|
489
|
+
const LocaleContext = createContext<{
|
|
490
|
+
locale: 'ar' | 'en';
|
|
491
|
+
setLocale: (locale: 'ar' | 'en') => Promise<void>;
|
|
492
|
+
}>(null);
|
|
493
|
+
|
|
494
|
+
export function LocaleProvider({ children }) {
|
|
495
|
+
const [locale, setLocaleState] = useState<'ar' | 'en'>('ar');
|
|
496
|
+
const [loading, setLoading] = useState(true);
|
|
497
|
+
|
|
498
|
+
useEffect(() => {
|
|
499
|
+
// Fetch user profile on mount
|
|
500
|
+
async function initLocale() {
|
|
501
|
+
try {
|
|
502
|
+
const profile = await fetchUserProfile();
|
|
503
|
+
const userLocale = profile.locale || 'ar';
|
|
504
|
+
setLocaleState(userLocale);
|
|
505
|
+
|
|
506
|
+
// Apply to DOM/RN
|
|
507
|
+
if (typeof document !== 'undefined') {
|
|
508
|
+
document.documentElement.dir = userLocale === 'ar' ? 'rtl' : 'ltr';
|
|
509
|
+
document.documentElement.lang = userLocale;
|
|
510
|
+
}
|
|
511
|
+
} catch (err) {
|
|
512
|
+
// Fallback to device locale
|
|
513
|
+
setLocaleState('ar');
|
|
514
|
+
} finally {
|
|
515
|
+
setLoading(false);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
initLocale();
|
|
520
|
+
}, []);
|
|
521
|
+
|
|
522
|
+
const setLocale = async (newLocale: 'ar' | 'en') => {
|
|
523
|
+
try {
|
|
524
|
+
// Update server
|
|
525
|
+
await updateUserProfile({ locale: newLocale });
|
|
526
|
+
|
|
527
|
+
// Update local state
|
|
528
|
+
setLocaleState(newLocale);
|
|
529
|
+
|
|
530
|
+
// Update DOM
|
|
531
|
+
if (typeof document !== 'undefined') {
|
|
532
|
+
document.documentElement.dir = newLocale === 'ar' ? 'rtl' : 'ltr';
|
|
533
|
+
document.documentElement.lang = newLocale;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Persist to device storage
|
|
537
|
+
if (typeof localStorage !== 'undefined') {
|
|
538
|
+
localStorage.setItem('locale', newLocale);
|
|
539
|
+
}
|
|
540
|
+
} catch (err) {
|
|
541
|
+
console.error('Failed to change locale:', err);
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
return (
|
|
546
|
+
<LocaleContext.Provider value={{ locale, setLocale }}>
|
|
547
|
+
{!loading && children}
|
|
548
|
+
</LocaleContext.Provider>
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function useLocale() {
|
|
553
|
+
const context = useContext(LocaleContext);
|
|
554
|
+
if (!context) throw new Error('useLocale must be used within LocaleProvider');
|
|
555
|
+
return context;
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
## Bidirectional Text
|
|
560
|
+
|
|
561
|
+
### Mixed-Direction Content
|
|
562
|
+
|
|
563
|
+
When Arabic and English text mix (e.g., product names, brand names), use Unicode bidirectional formatting to prevent visual corruption.
|
|
564
|
+
|
|
565
|
+
**Example Problem:**
|
|
566
|
+
```
|
|
567
|
+
Arabic: "اسم المنتج Product Name"
|
|
568
|
+
(text-align changes based on first character)
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**Solution: Use `<bdi>` tag or Unicode Isolates**
|
|
572
|
+
|
|
573
|
+
```jsx
|
|
574
|
+
// Automatically isolate direction
|
|
575
|
+
<p>الشركة: <bdi>Apple Inc.</bdi></p>
|
|
576
|
+
// → "الشركة: Apple Inc." (email correct)
|
|
577
|
+
|
|
578
|
+
// Or with span and dir attribute
|
|
579
|
+
<span dir="auto">مرحبا Hello</span>
|
|
580
|
+
// → Direction auto-detected from content
|
|
581
|
+
|
|
582
|
+
// Unicode isolates (lowest-level)
|
|
583
|
+
<p>{'الشركة: \u2066' + 'Apple Inc.' + '\u2069'}</p>
|
|
584
|
+
// \u2066 = LRI (Left-to-Right Isolate)
|
|
585
|
+
// \u2069 = PDI (Pop Directional Isolate)
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Phone Numbers, Emails, URLs (Always LTR)
|
|
589
|
+
|
|
590
|
+
```jsx
|
|
591
|
+
// Phone number in RTL context — always LTR
|
|
592
|
+
<p>
|
|
593
|
+
الهاتف: <span dir="ltr">+962 6 123 4567</span>
|
|
594
|
+
</p>
|
|
595
|
+
|
|
596
|
+
// Email address
|
|
597
|
+
<a href="mailto:user@example.com" dir="ltr">
|
|
598
|
+
user@example.com
|
|
599
|
+
</a>
|
|
600
|
+
|
|
601
|
+
// URL in Arabic text
|
|
602
|
+
<p>
|
|
603
|
+
زيارة الموقع: <span dir="ltr">https://example.com</span>
|
|
604
|
+
</p>
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### User-Generated Content
|
|
608
|
+
|
|
609
|
+
For text from users (comments, descriptions), use `<bdi>` to auto-detect direction:
|
|
610
|
+
|
|
611
|
+
```jsx
|
|
612
|
+
function UserComment({ text, locale }) {
|
|
613
|
+
return (
|
|
614
|
+
<div className={locale === 'ar' ? 'text-right' : 'text-left'}>
|
|
615
|
+
<bdi>{text}</bdi>
|
|
616
|
+
{/* Auto-detects whether text is RTL or LTR */}
|
|
617
|
+
</div>
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
## Common Mistakes & How to Avoid
|
|
623
|
+
|
|
624
|
+
### 1. Using Physical Properties Instead of Logical
|
|
625
|
+
|
|
626
|
+
```jsx
|
|
627
|
+
// ❌ Wrong: Text will be on the wrong side in RTL
|
|
628
|
+
<div className="ml-4 float-left">Content</div>
|
|
629
|
+
|
|
630
|
+
// ✓ Correct: Automatically flips in RTL
|
|
631
|
+
<div className="ms-4 float-start">Content</div>
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### 2. Forgetting to Flip Icons
|
|
635
|
+
|
|
636
|
+
```jsx
|
|
637
|
+
// ❌ Wrong: Arrow always points right
|
|
638
|
+
<button>
|
|
639
|
+
Next <ChevronRight />
|
|
640
|
+
</button>
|
|
641
|
+
|
|
642
|
+
// ✓ Correct: Icon flips based on locale
|
|
643
|
+
function NextButton() {
|
|
644
|
+
const { locale } = useLocale();
|
|
645
|
+
return (
|
|
646
|
+
<button className={locale === 'ar' ? 'flex-row-reverse' : ''}>
|
|
647
|
+
Next
|
|
648
|
+
<ChevronRight className={locale === 'ar' ? 'rotate-180' : ''} />
|
|
649
|
+
</button>
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### 3. Testing with Latin Placeholders Hides RTL Bugs
|
|
655
|
+
|
|
656
|
+
```
|
|
657
|
+
❌ "Lorem ipsum" in RTL container looks correct
|
|
658
|
+
✓ "مرحبا بك" (Arabic) immediately reveals direction issues
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
**Always test with actual Arabic text**, not Lorem Ipsum.
|
|
662
|
+
|
|
663
|
+
### 4. Hardcoding Text Alignment
|
|
664
|
+
|
|
665
|
+
```jsx
|
|
666
|
+
// ❌ Wrong: Alignment is fixed regardless of direction
|
|
667
|
+
<h1 style={{ textAlign: 'left' }}>Title</h1>
|
|
668
|
+
|
|
669
|
+
// ✓ Correct: Flips based on locale
|
|
670
|
+
<h1 className="text-start">Title</h1>
|
|
671
|
+
// or
|
|
672
|
+
<h1 style={{ textAlign: locale === 'ar' ? 'right' : 'left' }}>Title</h1>
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### 5. Not Isolating Mixed-Direction Content
|
|
676
|
+
|
|
677
|
+
```jsx
|
|
678
|
+
// ❌ Wrong: "Product Name" gets pulled to the wrong side
|
|
679
|
+
<p>المنتج: Product Name</p>
|
|
680
|
+
|
|
681
|
+
// ✓ Correct: Uses bidirectional isolation
|
|
682
|
+
<p>المنتج: <bdi>Product Name</bdi></p>
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### 6. Using Eastern Arabic Numerals (٠–٩)
|
|
686
|
+
|
|
687
|
+
```
|
|
688
|
+
❌ "السعر: ١٢٣٤"
|
|
689
|
+
✓ "السعر: 1234"
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
Always use Western numerals (0–9), even in Arabic UI.
|
|
693
|
+
|
|
694
|
+
## Recommended Libraries
|
|
695
|
+
|
|
696
|
+
### Web (Next.js or React)
|
|
697
|
+
|
|
698
|
+
- **`next-intl`** — Built for Next.js, excellent RTL support, type-safe message keys
|
|
699
|
+
```bash
|
|
700
|
+
npm install next-intl
|
|
701
|
+
```
|
|
702
|
+
Configuration in `next.config.js`, message files in `messages/` directory.
|
|
703
|
+
|
|
704
|
+
- **`react-i18next`** — Popular, flexible, works with any React setup
|
|
705
|
+
```bash
|
|
706
|
+
npm install i18next react-i18next
|
|
707
|
+
```
|
|
708
|
+
Share translation files with backend/mobile via `@sakani/shared`.
|
|
709
|
+
|
|
710
|
+
### Mobile (React Native/Expo)
|
|
711
|
+
|
|
712
|
+
- **`react-i18next`** — Same library as web, share translation files
|
|
713
|
+
- **`expo-localization`** — Get device locale
|
|
714
|
+
```bash
|
|
715
|
+
npx expo install expo-localization
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Shared (Backend & Frontend)
|
|
719
|
+
|
|
720
|
+
- **`i18next`** — Core i18n engine, language-agnostic
|
|
721
|
+
- Message files in JSON or TypeScript in shared package (`@sakani/shared/i18n/`)
|
|
722
|
+
- Both frontend and backend import from shared package
|
|
723
|
+
|
|
724
|
+
### Date & Time
|
|
725
|
+
|
|
726
|
+
- **`date-fns`** — Lightweight, tree-shakeable, excellent locale support
|
|
727
|
+
```bash
|
|
728
|
+
npm install date-fns
|
|
729
|
+
```
|
|
730
|
+
Import locales: `import { ar } from 'date-fns/locale'`
|
|
731
|
+
|
|
732
|
+
- **Native `Intl` API** — Built-in, no dependencies, sufficient for most use cases
|
|
733
|
+
```typescript
|
|
734
|
+
new Intl.DateTimeFormat('ar-JO', { ... }).format(date)
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
## Summary Checklist
|
|
738
|
+
|
|
739
|
+
- [ ] All UI strings use message keys (`key.domain.entity`)
|
|
740
|
+
- [ ] Backend returns message keys, never display text
|
|
741
|
+
- [ ] Frontend resolves keys via i18n library (next-intl, react-i18next, etc.)
|
|
742
|
+
- [ ] Web: `tailwindcss-rtl` installed, using logical properties (`ms-`, `ps-`, `text-start`)
|
|
743
|
+
- [ ] Web: Root `dir="rtl"` set when locale is Arabic
|
|
744
|
+
- [ ] Mobile: `I18nManager.forceRTL(isArabic)` on app init
|
|
745
|
+
- [ ] Icons tested and flipped for RTL (chevrons, arrows)
|
|
746
|
+
- [ ] Date formatting uses `Intl.DateTimeFormat` or `date-fns` with locale
|
|
747
|
+
- [ ] All numerals are Western (0–9), never Eastern (٠–٩)
|
|
748
|
+
- [ ] Mixed-direction content (Arabic + English) uses `<bdi>` or `dir="auto"`
|
|
749
|
+
- [ ] Phone numbers, emails, URLs wrapped in `dir="ltr"`
|
|
750
|
+
- [ ] Locale preference stored in user profile (synced) + device storage (offline)
|
|
751
|
+
- [ ] Locale switcher tested: changes are persisted and UI updates immediately
|
|
752
|
+
- [ ] RTL testing done with **actual Arabic text**, not placeholders
|