kreftjs 0.0.17-beta → 0.0.20-beta
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 +20 -0
- package/bin/build.js +13 -2
- package/bin/electron.js +61 -2
- package/bin/kreftjs.js +5 -3
- package/book/ar/01-getting-started.md +169 -0
- package/book/ar/02-language.md +330 -0
- package/book/ar/03-components-and-state.md +258 -0
- package/book/ar/04-router-data-forms.md +333 -0
- package/book/ar/05-arabic-rtl.md +175 -0
- package/book/ar/06-backend.md +247 -0
- package/book/ar/07-auth-and-security.md +157 -0
- package/book/ar/08-background-work.md +250 -0
- package/book/ar/09-testing-and-deploy.md +234 -0
- package/book/ar/10-electron-desktop.md +258 -0
- package/book/ar/11-advanced-blog.md +370 -0
- package/book/ar/12-advanced-shop.md +426 -0
- package/book/ar/13-advanced-ui.md +440 -0
- package/book/ar/14-advanced-ops.md +167 -0
- package/book/ar/15-appendix.md +81 -0
- package/book/ar/README.md +62 -0
- package/book/kreft-book-ar.docx +0 -0
- package/docs/README.md +43 -8
- package/kreftjs.d.ts +1 -1
- package/kreftjs.js +436 -74
- package/package.json +4 -3
- package/templates/app/frontend/errors/ErrorMonitor.kreft +2 -2
- package/templates/app/frontend/kreftjs.d.ts +1 -1
- package/templates/app/frontend/kreftjs.js +436 -74
- package/templates/app/frontend/pages/Dashboard.kreft +1 -1
- package/templates/errors/ErrorMonitor.kreft +1 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# الفصل الرابع: الموجِّه والبيانات والنماذج
|
|
2
|
+
|
|
3
|
+
## تعريف المسارات
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
const router = Kreftjs.router({
|
|
7
|
+
routes: [
|
|
8
|
+
{ path: '/', component: 'pages/Home.kreft', meta: { title: 'الرئيسية' } },
|
|
9
|
+
{ path: '/users/:id', name: 'user', component: 'pages/User.kreft', props: true },
|
|
10
|
+
{ path: '/posts/:slug?', component: 'pages/Posts.kreft' },
|
|
11
|
+
{ path: '/old', redirect: '/' },
|
|
12
|
+
{ path: '/admin', component: 'pages/Admin.kreft', guard: () => isAdmin() || '/login' },
|
|
13
|
+
{ path: '*', component: 'pages/NotFound.kreft' },
|
|
14
|
+
],
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
Kreftjs.app('App.kreft').use(router).mount('#app')
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<!-- App.kreft -->
|
|
22
|
+
<view>
|
|
23
|
+
<nav>
|
|
24
|
+
<kreft-link to="/">الرئيسية</kreft-link>
|
|
25
|
+
<kreft-link to="/users/{user.id}">الملف الشخصي</kreft-link>
|
|
26
|
+
</nav>
|
|
27
|
+
<kreft-view></kreft-view>
|
|
28
|
+
</view>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
مع `props: true` يصل المعامل `:id` كـ prop:
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<!-- pages/User.kreft -->
|
|
35
|
+
prop id
|
|
36
|
+
derive user = users.find(id)
|
|
37
|
+
action back() { $router.back() }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| الميزة | الطريقة |
|
|
41
|
+
|---|---|
|
|
42
|
+
| المسار الحالي | `$route.path` و`$route.params.id` و`$route.query.tab` و`$route.meta` |
|
|
43
|
+
| التنقل | `$router.go('/users/5?tab=info')` و`$router.replace(to)` و`$router.back()` |
|
|
44
|
+
| الرابط النشط | الفئتان `kreft-link-active` و`kreft-link-exact` |
|
|
45
|
+
| عنوان الصفحة | `meta: { title: '…' }` |
|
|
46
|
+
| فحص قبل كل صفحة | `router.before((to, from) => …)`. أرجع `false` للإيقاف أو مسارًا للذهاب إليه. |
|
|
47
|
+
| فحص مسار واحد | `guard: (to, from) => …` |
|
|
48
|
+
| بعد كل صفحة | `router.after((to, from) => …)` |
|
|
49
|
+
|
|
50
|
+
كل ملف صفحة يُحمَّل أول مرة يفتحها أحد.
|
|
51
|
+
|
|
52
|
+
## ملفات المسارات
|
|
53
|
+
|
|
54
|
+
المشاريع الجديدة تحفظ صفحاتها في `frontend/routes/web.kreft`، و`index.html` يشير إليه فقط:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
Kreftjs.router({
|
|
58
|
+
routes: ['routes/web.kreft', 'routes/shop.kreft'],
|
|
59
|
+
layout: 'app',
|
|
60
|
+
errors: 'errors',
|
|
61
|
+
middleware: {
|
|
62
|
+
auth: (to) => (auth.user ? true : '/login?redirect=' + encodeURIComponent(to.fullPath)),
|
|
63
|
+
guest: () => (auth.user ? '/dashboard' : true),
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
// routes/web.kreft
|
|
70
|
+
Route.view('/', 'pages/Home.kreft').name('home').title('الرئيسية')
|
|
71
|
+
Route.view('/posts/:id', 'pages/Post.kreft')
|
|
72
|
+
|
|
73
|
+
Route.middleware('auth').group(() => {
|
|
74
|
+
Route.view('/dashboard', 'pages/Dashboard.kreft').middleware('verified')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
Route.layout('guest').middleware('guest').group(() => {
|
|
78
|
+
Route.view('/login', 'pages/auth/Login.kreft').title('تسجيل الدخول')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
Route.prefix('/account').name('account.').middleware('auth').group(() => {
|
|
82
|
+
Route.view('/settings', 'pages/account/Settings.kreft').children(() => {
|
|
83
|
+
Route.view('', 'pages/account/General.kreft') // /account/settings
|
|
84
|
+
Route.view('mail', 'pages/account/Mail.kreft') // /account/settings/mail
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
Route.redirect('/home', '/')
|
|
89
|
+
Route.fallback('pages/NotFound.kreft')
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| على المسار | ماذا يفعل |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `.name('home')` | `$router.go({ name: 'home' })` |
|
|
95
|
+
| `.title('Home')` | عنوان الصفحة. |
|
|
96
|
+
| `.layout('guest')` و`.layout('none')` | تخطيط آخر أو بلا تخطيط. |
|
|
97
|
+
| `.middleware('auth', 'role:editor')` | ينفّذ هذه قبل الصفحة. |
|
|
98
|
+
| `.meta({ … })` | أي شيء آخر، في `$route.meta`. |
|
|
99
|
+
| `.children(() => …)` | صفحات متداخلة تظهر في `<kreft-view>` الخاص بالصفحة. |
|
|
100
|
+
|
|
101
|
+
> **ملاحظة:** وسيط `auth` في المتصفح يقرر أي الشاشات تُعرض فقط. هو لا يحمي البيانات: كل مسار API يجب أن يفحص المستخدم على الخادم.
|
|
102
|
+
|
|
103
|
+
## التخطيطات (Layouts)
|
|
104
|
+
|
|
105
|
+
التخطيط هو الإطار حول صفحاتك: الشريط والقائمة والتذييل. ضعه في `layouts/` واستعمل `<slot>` حيث تذهب الصفحة.
|
|
106
|
+
|
|
107
|
+
```html
|
|
108
|
+
<!-- layouts/app.kreft -->
|
|
109
|
+
state menuOpen = false
|
|
110
|
+
|
|
111
|
+
<view>
|
|
112
|
+
<header>…</header>
|
|
113
|
+
<main><slot></slot></main>
|
|
114
|
+
</view>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| أين | ماذا تكتب |
|
|
118
|
+
|---|---|
|
|
119
|
+
| كل الصفحات | `Kreftjs.router({ layout: 'app', routes })` |
|
|
120
|
+
| مسار واحد وأبناؤه | `{ path: '/admin', component: …, layout: 'admin' }` |
|
|
121
|
+
| داخل ملف صفحة | `layout admin` في سطر وحده |
|
|
122
|
+
| بلا تخطيط | `layout: false` على المسار أو `layout none` في الصفحة |
|
|
123
|
+
|
|
124
|
+
عند انطباق أكثر من واحد: المسار يغلب، ثم ملف الصفحة، ثم الموجِّه. التنقل بين صفحتين تستعملان التخطيط نفسه يُبقي التخطيط على الشاشة مع حالته.
|
|
125
|
+
|
|
126
|
+
وللصفحة أن تملأ أماكن أخرى في تخطيطها:
|
|
127
|
+
|
|
128
|
+
```html
|
|
129
|
+
<!-- في التخطيط -->
|
|
130
|
+
<kreft-yield name="actions">يظهر إن لم يكن للصفحة هذا القسم</kreft-yield>
|
|
131
|
+
|
|
132
|
+
<!-- في الصفحة -->
|
|
133
|
+
<kreft-section name="actions">
|
|
134
|
+
<button class="primary" @click="save()">حفظ</button>
|
|
135
|
+
</kreft-section>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## صفحات الأخطاء
|
|
139
|
+
|
|
140
|
+
لكل خطأ HTTP صفحته في `errors/`. فعّلها بـ `errors: 'errors'` في الموجِّه:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
errors/401.kreft 402.kreft 403.kreft 404.kreft 419.kreft 429.kreft 500.kreft 503.kreft
|
|
144
|
+
errors/error.kreft أي حالة أخرى
|
|
145
|
+
errors/minimal.kreft الإطار المشترك
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
عنوان غير معروف يعرض `errors/404.kreft`. ومن أي ملف `.kreft` يعرض `abort(404)` الصفحة المناسبة:
|
|
149
|
+
|
|
150
|
+
```html
|
|
151
|
+
prop id
|
|
152
|
+
load post = api.get('/api/posts/' + id)
|
|
153
|
+
watch post.error { if (post.error && post.error.status === 404) abort(404) }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## العناوين النظيفة
|
|
157
|
+
|
|
158
|
+
يستعمل الموجِّه افتراضيًا عناوين بلا `#` (`/users/5`). ويلزم لذلك أمران:
|
|
159
|
+
|
|
160
|
+
1. وسم `<base href="/">` في `index.html` (استعمل `<base href="/demo/">` إن كان التطبيق في مجلد).
|
|
161
|
+
2. أن يرسل الخادم `index.html` لكل مسار صفحة، حتى لا يكون إعادة تحميل `/users/5` خطأ 404. الأمر `npm run dev` يفعل هذا عنك.
|
|
162
|
+
|
|
163
|
+
> **تنبيه مهم عند النشر:** إن بنيت الموقع بقاعدة خاطئة (مثلًا `/demo/`) وهو في الجذر، ستظهر صفحة بيضاء لأن المتصفح يطلب الملفات من `/demo/app.js`. ابنِ بـ `kreft build --base /`. تفاصيل أكثر في فصل النشر.
|
|
164
|
+
|
|
165
|
+
إن لم تستطع تغيير الخادم فاستعمل `mode: 'hash'`؛ فتصير العناوين `/#/users/5`.
|
|
166
|
+
|
|
167
|
+
## جلب البيانات
|
|
168
|
+
|
|
169
|
+
العميل العام `api` متاح في كل ملف `.kreft`، وكذلك `Kreftjs.api` في JavaScript العادي:
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
const users = await api.get('/api/users', { query: { page: 2 } })
|
|
173
|
+
const user = await api.post('/api/users', { name: 'سارة أحمد' })
|
|
174
|
+
await api.put('/api/users/1', user)
|
|
175
|
+
await api.patch('/api/users/1', { status: 'active' })
|
|
176
|
+
await api.delete('/api/users/1')
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### load: تحميل البيانات
|
|
180
|
+
|
|
181
|
+
`load` يعلن مصدرًا تفاعليًا. يتولى حالة التحميل والخطأ ويجلب من جديد إذا تغيّرت القيم التي يقرؤها:
|
|
182
|
+
|
|
183
|
+
```html
|
|
184
|
+
<!-- pages/Posts.kreft -->
|
|
185
|
+
state query = ''
|
|
186
|
+
state page = 1
|
|
187
|
+
|
|
188
|
+
load posts = api.get('/api/posts', { query: { q: query || undefined, page } })
|
|
189
|
+
|
|
190
|
+
<view>
|
|
191
|
+
<input type="search" k-model.lazy="query" placeholder="ابحث في المقالات…">
|
|
192
|
+
|
|
193
|
+
<p k-if="posts.loading">جارٍ تحميل المقالات…</p>
|
|
194
|
+
<div k-elif="posts.error">
|
|
195
|
+
فشل التحميل: {posts.error.message}
|
|
196
|
+
<button @click={posts.reload}>حاول مجددًا</button>
|
|
197
|
+
</div>
|
|
198
|
+
<div k-else>
|
|
199
|
+
{for post in posts.data || [] key post.id}
|
|
200
|
+
<article><h3>{post.title}</h3><p>{post.body}</p></article>
|
|
201
|
+
{empty}
|
|
202
|
+
<p>لا نتائج لـ "{query}".</p>
|
|
203
|
+
{/for}
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<button @click={() => page++}>الصفحة التالية ({page})</button>
|
|
207
|
+
</view>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
| الخاصية | المعنى |
|
|
211
|
+
|---|---|
|
|
212
|
+
| `posts.data` | النتيجة (`null` حتى تُحمَّل). |
|
|
213
|
+
| `posts.loading` | `true` أثناء الطلب. |
|
|
214
|
+
| `posts.loaded` | `true` بعد وصول أول جواب. |
|
|
215
|
+
| `posts.error` | الخطأ أو `null`. |
|
|
216
|
+
| `posts.reload()` | اجلب الآن من جديد. |
|
|
217
|
+
|
|
218
|
+
### الجداول
|
|
219
|
+
|
|
220
|
+
للجداول التي يقدّمها `kreft dev --db` أو واجهة الجداول التلقائية، يعطيك `table(name)` استدعاءات جاهزة:
|
|
221
|
+
|
|
222
|
+
```html
|
|
223
|
+
const posts = table('posts')
|
|
224
|
+
|
|
225
|
+
load list = posts.all({ q: search, _sort: '-created_at', _limit: 10 })
|
|
226
|
+
|
|
227
|
+
action createPost() {
|
|
228
|
+
const created = await posts.create({ title: 'مقال جديد' })
|
|
229
|
+
list.data = [created, ...list.data]
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
الدوال: `all(query)` و`find(id)` و`count(query)` و`create(data)` و`update(id, data)` (PUT) و`patch(id, data)` و`remove(id)`.
|
|
234
|
+
|
|
235
|
+
### الأخطاء
|
|
236
|
+
|
|
237
|
+
الطلب الفاشل يرمي `ApiError` فيه `status` و`message` و`data` و`errors` (رسائل التحقق). مشاكل الشبكة وانتهاء المهلة لها `status` يساوي 0.
|
|
238
|
+
|
|
239
|
+
```html
|
|
240
|
+
state errors = {}
|
|
241
|
+
|
|
242
|
+
action save() {
|
|
243
|
+
try {
|
|
244
|
+
await api.post('/api/users', form)
|
|
245
|
+
} catch (e) {
|
|
246
|
+
errors = e.errors || {} // { email: ['The email has already been taken.'] }
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### الإعدادات
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
Kreftjs.api.setup({
|
|
255
|
+
base: '/api', // فيصير api.get('users') هو /api/users
|
|
256
|
+
token: () => Kreftjs.storage.get('token'), // يُرسل كـ Authorization: Bearer …
|
|
257
|
+
headers: { 'Accept-Language': 'ar' },
|
|
258
|
+
timeout: 10000,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
Kreftjs.api.onError((e) => { if (e.status === 401) location.href = '/login' })
|
|
262
|
+
|
|
263
|
+
const github = Kreftjs.api.create({ base: 'https://api.github.com' }) // عميل ثانٍ
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
خيارات للطلب الواحد: `query` و`headers` و`timeout` و`signal` (للإلغاء) و`full: true` (يعيد `{ data, status, headers }`).
|
|
267
|
+
|
|
268
|
+
الطلبات إلى خادمك تُرسل أيضًا `X-XSRF-TOKEN` من كوكي `XSRF-TOKEN` (و`X-CSRF-TOKEN` من `<meta name="csrf-token">`)، و`X-Requested-With`.
|
|
269
|
+
|
|
270
|
+
### واجهة تجريبية للتدرّب
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
kreft dev --api db.json # واجهة REST على /api من ملف JSON
|
|
274
|
+
kreft dev --api db.json --delay 500 # أجب ببطء لترى حالات التحميل
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
> **تحذير:** `--api` و`--db` للتطوير فقط. لا تسجيل دخول فيهما. في الإنتاج اكتب مسارات حقيقية.
|
|
278
|
+
|
|
279
|
+
## النماذج والتحقق
|
|
280
|
+
|
|
281
|
+
أعطِ النموذج قواعده بأسلوب تصريحي. النموذج الذي فيه أخطاء لا يُرسل أبدًا:
|
|
282
|
+
|
|
283
|
+
```html
|
|
284
|
+
state post = form({ title: '', email: '' }, { title: 'required|min:3|max:120', email: 'required|email' })
|
|
285
|
+
|
|
286
|
+
action save() {
|
|
287
|
+
const created = await post.$submit((values) => api.post('/api/posts', values))
|
|
288
|
+
if (created) post.$reset()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
<view>
|
|
292
|
+
<form @submit.prevent={save}>
|
|
293
|
+
<input k-model="post.title">
|
|
294
|
+
<p k-if="post.$error('title')" class="error">{post.$error('title')}</p>
|
|
295
|
+
<button disabled={post.$busy}>حفظ</button>
|
|
296
|
+
</form>
|
|
297
|
+
</view>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
| على النموذج | ما هو |
|
|
301
|
+
|---|---|
|
|
302
|
+
| `post.title` و`post.email` | القيم، لـ `k-model`. |
|
|
303
|
+
| `post.$error('title')` | أول رسالة للحقل أو `''`. |
|
|
304
|
+
| `post.$errors` | كل الرسائل: `{ title: ['…'] }`. |
|
|
305
|
+
| `post.$valid` و`post.$busy` | لا أخطاء / يُرسل الآن. |
|
|
306
|
+
| `post.$submit(send)` | يفحص ثم يستدعي `send(values)`. وخطأ 422 من الخادم يملأ الأخطاء نفسها. يعيد `false` إن لم يُرسل. |
|
|
307
|
+
| `post.$check()` و`post.$reset(values?)` و`post.$setErrors(e)` | افحص الآن / ابدأ من جديد / اعرض أخطاء من مكان آخر. |
|
|
308
|
+
|
|
309
|
+
تظهر الأخطاء بعد أول إرسال، ثم تتبع كتابتك.
|
|
310
|
+
|
|
311
|
+
بلا نموذج: `validate(values, rules)` يعطي `{}` أو `{ field: ['…'] }`. ورسائلك الخاصة في الوسيط الثالث:
|
|
312
|
+
|
|
313
|
+
```js
|
|
314
|
+
form(values, rules, { messages: { 'title.required': 'اكتب عنوانًا.' } })
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
أسماء القواعد نفسها تعمل على الخادم في `req.validate()`، فمجموعة قواعد واحدة تصف الطرفين.
|
|
318
|
+
|
|
319
|
+
### كلمات المرور
|
|
320
|
+
|
|
321
|
+
القاعدة `not_common_password` ترفض الكلمات الشائعة (`password` و`12345678` …) وحرفًا مكرّرًا (`aaaaaaaa`) وتسلسلًا بسيطًا (`abcdefgh`) وما يطابق البريد. تستعملها المشاريع الجديدة مع `min:8`:
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
password: 'required|min:8|confirmed|not_common_password'
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
هي قائمة قصيرة تعمل دون اتصال، فتمسك أسوأ الاختيارات فقط. ارفع `min` لما هو أطول (عبارة من أربع كلمات خير من خليط قصير من الرموز).
|
|
328
|
+
|
|
329
|
+
## تمارين
|
|
330
|
+
|
|
331
|
+
1. اكتب صفحة `Posts` فيها بحث وترقيم صفحات بـ `load`، مع زر "حاول مجددًا" عند الخطأ.
|
|
332
|
+
2. اكتب نموذج تسجيل بقواعد `required|email` و`required|min:8|confirmed`، وارسمه برسائل خطأ بالعربية.
|
|
333
|
+
3. أنشئ مسارات متداخلة: `/account/settings` مع تبويبين `general` و`mail`.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# الفصل الخامس: العربية والترجمة والاتجاه (RTL)
|
|
2
|
+
|
|
3
|
+
Kreft يترجم النصوص ويقلب الاتجاه للغات التي تُكتب من اليمين إلى اليسار: العربية والعبرية والفارسية والأردية. هذا الفصل مخصص لنا.
|
|
4
|
+
|
|
5
|
+
## ملفات الترجمة
|
|
6
|
+
|
|
7
|
+
المشروع الجديد يأتي بمجلد `lang/` في جذره، وفيه العربية والإنجليزية جاهزتان: `lang/ar.json` و`lang/ar/messages.json` و`lang/ar/validation.json` و`lang/ar/pagination.json` (ويُقرأ أيضًا `frontend/lang/` و`backend/lang/` و`resources/lang/`). استعمل `lang/ar.json` للنصوص البسيطة، أو `lang/ar/messages.json` للمفاتيح المجمّعة.
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
// lang/ar/messages.json
|
|
11
|
+
{
|
|
12
|
+
"welcome": "أهلًا بك في تطبيقي",
|
|
13
|
+
"hello": "مرحبًا، :name",
|
|
14
|
+
"items": "{0} لا توجد عناصر|{1} عنصر واحد|{2} عنصران|[3,10] :count عناصر|[11,*] :count عنصرًا"
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
// lang/en/messages.json
|
|
20
|
+
{
|
|
21
|
+
"welcome": "Welcome to my app",
|
|
22
|
+
"hello": "Hello, :name",
|
|
23
|
+
"items": "{0} No items|{1} One item|[2,*] :count items"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
وفي أي صفحة:
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<h1>{$t('messages.hello', { name: user.name })}</h1>
|
|
31
|
+
<p>{$choice('messages.items', count)}</p>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- الرسالة تستعمل `:name` للقيم.
|
|
35
|
+
- للجمع افصل الصيغ بـ `|`، وحدّد المجال بين `{}` للقيمة الواحدة (`{0}` و`{2}`) أو `[من,إلى]` (`[3,10]` و`[11,*]`). هذا يناسب العربية تمامًا: صفر، وواحد، واثنان، وجمع القلة (3–10)، وما فوق (11+) بصيغة التمييز المنصوب.
|
|
36
|
+
|
|
37
|
+
## تبديل اللغة
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
Kreftjs.locale('ar') // يتبدّل <html lang> و dir وتُعاد رسم كل الصفحات
|
|
41
|
+
Kreftjs.fallbackLocale('en') // اللغة الاحتياطية
|
|
42
|
+
Kreftjs.locales() // اللغات المحمّلة
|
|
43
|
+
Kreftjs.translations('ar', { 'app.title': 'تطبيقي' }) // نصوص إضافية من الشيفرة
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## مبدّل لغة كامل (مع التذكّر)
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<!-- components/LangSwitch.kreft -->
|
|
50
|
+
saved lang = 'ar' as 'app.lang'
|
|
51
|
+
|
|
52
|
+
watch lang { Kreftjs.locale(value) }
|
|
53
|
+
on start { Kreftjs.locale(lang) }
|
|
54
|
+
|
|
55
|
+
<view>
|
|
56
|
+
<button class="lang" @click="lang = lang === 'ar' ? 'en' : 'ar'">
|
|
57
|
+
{lang === 'ar' ? 'English' : 'العربية'}
|
|
58
|
+
</button>
|
|
59
|
+
</view>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- `saved` يتذكر الاختيار بعد إعادة التحميل.
|
|
63
|
+
- `watch` يطبّق اللغة عند كل تغيير.
|
|
64
|
+
- `on start` يطبّقها عند فتح الصفحة.
|
|
65
|
+
|
|
66
|
+
## الاتجاه
|
|
67
|
+
|
|
68
|
+
يُضبط `dir` تلقائيًا من اللغة، ويمكنك ربطه بيدك:
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<view dir={isArabic ? 'rtl' : 'ltr'}>…</view>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## اكتب CSS ينقلب وحده
|
|
75
|
+
|
|
76
|
+
استعمل الخصائص المنطقية (logical properties) بدل left/right:
|
|
77
|
+
|
|
78
|
+
| تجنّب | استعمل |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `margin-left` / `margin-right` | `margin-inline-start` / `margin-inline-end` |
|
|
81
|
+
| `padding-left` / `padding-right` | `padding-inline-start` / `padding-inline-end` |
|
|
82
|
+
| `left: 0` / `right: 0` | `inset-inline-start: 0` / `inset-inline-end: 0` |
|
|
83
|
+
| `text-align: left` / `right` | `text-align: start` / `end` |
|
|
84
|
+
| `border-left` / `border-right` | `border-inline-start` / `border-inline-end` |
|
|
85
|
+
|
|
86
|
+
```css
|
|
87
|
+
.sidebar {
|
|
88
|
+
position: fixed;
|
|
89
|
+
inset-inline-start: 0; /* يسار في LTR، يمين في RTL */
|
|
90
|
+
padding-inline-start: 24px;
|
|
91
|
+
border-inline-end: 1px solid var(--border);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
[dir="rtl"] .back-arrow { transform: scaleX(-1); } /* اقلب أيقونة */
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## نصائح عملية للتطبيقات العربية
|
|
98
|
+
|
|
99
|
+
1. **الأرقام:** استعمل `Intl.NumberFormat('ar-EG')` للأرقام الهندية أو `ar-SA`/`ar-MA` لما تفضله بلدك:
|
|
100
|
+
```html
|
|
101
|
+
derive fmt = new Intl.NumberFormat(lang === 'ar' ? 'ar-EG' : 'en-US')
|
|
102
|
+
<p>{fmt.format(total)}</p>
|
|
103
|
+
```
|
|
104
|
+
2. **التواريخ:** `new Date().toLocaleDateString('ar', { dateStyle: 'long' })`. اختر التقويم بـ `ar-SA-u-ca-islamic` إذا أردت الهجري.
|
|
105
|
+
3. **الخطوط:** اختر خطًا عربيًا (مثل Cairo أو Tajawal). في تطبيقات سطح المكتب لا تعتمد على تحميل خط من الإنترنت؛ ضمّن الخط محليًا.
|
|
106
|
+
4. **الأيقونات الاتجاهية:** الأسهم والرجوع تحتاج `scaleX(-1)` في RTL كما في المثال.
|
|
107
|
+
5. **النص المختلط:** اللاتينية داخل العربية قد تبدّل ترتيب علامات الترقيم. غلّف المقطع بـ `<bdi>` أو `unicode-bidi: isolate`.
|
|
108
|
+
6. **حقول الإدخال:** للبريد والرقم السري وأرقام الهاتف أضف `dir="ltr"` حتى تُكتب صحيحة.
|
|
109
|
+
|
|
110
|
+
```html
|
|
111
|
+
<input type="email" dir="ltr" k-model="form.email" placeholder="name@example.com">
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## على الخادم
|
|
115
|
+
|
|
116
|
+
لغة الطلب تأتي من `?lang=` أو كوكي `lang` أو `Accept-Language`، ورسائل الخادم تُقرأ من `lang/<locale>/`، والعربية جاهزة في المشروع الجديد:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
// lang/ar/validation.json (موجود أصلًا في المشروع الجديد)
|
|
120
|
+
{
|
|
121
|
+
"required": "حقل :attribute مطلوب.",
|
|
122
|
+
"email": "يجب أن يكون :attribute عنوان بريد إلكتروني صالح.",
|
|
123
|
+
"min": { "string": "يجب أن يحتوي :attribute على :min أحرف على الأقل." },
|
|
124
|
+
"attributes": { "email": "البريد الإلكتروني", "password": "كلمة المرور" },
|
|
125
|
+
"not_common_password": "كلمة المرور شائعة جدًا أو سهلة التخمين. اختر غيرها."
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
المفتاح `attributes` يترجم أسماء الحقول داخل الرسائل: فتظهر "حقل البريد الإلكتروني مطلوب" بدل "حقل email مطلوب".
|
|
130
|
+
|
|
131
|
+
وللتخصيص الدقيق لنموذج بعينه استعمل `messages()` في `FormRequest`.
|
|
132
|
+
|
|
133
|
+
## مثال: صفحة هبوط ثنائية اللغة مع SEO
|
|
134
|
+
|
|
135
|
+
```html
|
|
136
|
+
<!-- pages/Home.kreft -->
|
|
137
|
+
import LangSwitch from '../components/LangSwitch.kreft'
|
|
138
|
+
|
|
139
|
+
state count = 3
|
|
140
|
+
|
|
141
|
+
<view>
|
|
142
|
+
<header class="top">
|
|
143
|
+
<h1>{$t('messages.hello', { name: 'الزائر' })}</h1>
|
|
144
|
+
<lang-switch></lang-switch>
|
|
145
|
+
</header>
|
|
146
|
+
<p>{$choice('messages.items', count)}</p>
|
|
147
|
+
<button @click="count++">+</button>
|
|
148
|
+
</view>
|
|
149
|
+
|
|
150
|
+
<style scoped>
|
|
151
|
+
.top {
|
|
152
|
+
display: flex;
|
|
153
|
+
justify-content: space-between;
|
|
154
|
+
padding-inline: 24px;
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
ولظهور الصفحة صحيحة لمحركات البحث بلغتين، أعلن البدائل في `backend/routes/seo.js` (انظر فصل النشر):
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
module.exports = {
|
|
163
|
+
'/': {
|
|
164
|
+
title: 'متجري',
|
|
165
|
+
description: 'كل ما يحتاجه بيتك',
|
|
166
|
+
alternates: { en: 'https://example.com/', ar: 'https://example.com/ar/' },
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## تمارين
|
|
172
|
+
|
|
173
|
+
1. اجعل تطبيقك ثنائي اللغة (عربي/إنجليزي) واحفظ اللغة المختارة.
|
|
174
|
+
2. حوّل كل `margin-left` و`padding-right` في CSS تطبيقك إلى خصائص منطقية وجرّب التبديل.
|
|
175
|
+
3. اكتب مكوّن سعر يعرض المبلغ بالأرقام الهندية عند العربية.
|