Skip to content

Commit e54d35a

Browse files
committed
finalize i18n plugin, pluralization for i18n
1 parent 990bebd commit e54d35a

File tree

29 files changed

+396
-189
lines changed

29 files changed

+396
-189
lines changed

Changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Command to generate typescript models `npx -y adminforth generate-models --env-file=.env`
1313
- add i18n support: add vue-i18n to frontend and tr function to backend. This will allow to implement translation plugins
14+
- badgeTooltip - now you can add a tooltip to the badge to explain what it means
15+
- ability to authorize not only subscription on websocket but filter out whom users message will be published (updated doc)
16+
- added ability to refresh menu item badge from the backend using websocket publish
1417

1518
# Improved
1619

adminforth/dataConnectors/postgres.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
2727

2828
OperatorsMap = {
2929
[AdminForthFilterOperators.EQ]: '=',
30-
[AdminForthFilterOperators.NE]: '!=',
30+
[AdminForthFilterOperators.NE]: 'IS DISTINCT FROM',
3131
[AdminForthFilterOperators.GT]: '>',
3232
[AdminForthFilterOperators.LT]: '<',
3333
[AdminForthFilterOperators.GTE]: '>=',
@@ -215,6 +215,7 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
215215
totalCounter += 1;
216216
}
217217
218+
218219
if (fieldData._underlineType == 'uuid') {
219220
field = `cast("${field}" as text)`
220221
} else {
@@ -232,6 +233,8 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
232233
filterValues.push(`%${v}%`);
233234
} else if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
234235
filterValues.push(...v);
236+
237+
235238
} else {
236239
filterValues.push(v);
237240
}

adminforth/documentation/docs/tutorial/03-Customization/06-customPages.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,15 @@ Create a Vue component in the `custom` directory of your project, e.g. `Dashboar
3838
<template>
3939
<div class="px-4 py-8 bg-blue-50 dark:bg-gray-900 dark:shadow-none min-h-screen">
4040
<h1 class="mb-4 text-xl font-extrabold text-gray-900 dark:text-white md:text-2xl lg:text-3xl"><span
41-
class="text-transparent bg-clip-text bg-gradient-to-r to-emerald-600 from-sky-400">Apartments</span>
41+
class="text-transparent bg-clip-text bg-gradient-to-r to-emerald-600 from-sky-400">{{ $t('Apartments') }}</span>
4242
Statistics.</h1>
4343
4444
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
4545
<div class="max-w-md w-full bg-white rounded-lg shadow dark:bg-gray-800 p-4 md:p-6" v-if="data">
4646
<div class="flex justify-between">
4747
<div>
4848
<h5 class="leading-none text-3xl font-bold text-gray-900 dark:text-white pb-2">{{ data.totalAparts }}</h5>
49-
<p class="text-base font-normal text-gray-500 dark:text-gray-400">Apartments last 7 days</p>
49+
<p class="text-base font-normal text-gray-500 dark:text-gray-400">{{ $t('Apartment last 7 days | Apartments last 7 days', data.totalAparts) }}</p>
5050
</div>
5151
5252
</div>
@@ -58,15 +58,15 @@ Create a Vue component in the `custom` directory of your project, e.g. `Dashboar
5858
5959
<div class="grid grid-cols-2 py-3">
6060
<dl>
61-
<dt class="text-base font-normal text-gray-500 dark:text-gray-400 pb-1">Listed price</dt>
61+
<dt class="text-base font-normal text-gray-500 dark:text-gray-400 pb-1">{{ $t('Listed price') }}</dt>
6262
<dd class="leading-none text-xl font-bold text-green-500 dark:text-green-400">{{
6363
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(
6464
data.totalListedPrice,
6565
) }}
6666
</dd>
6767
</dl>
6868
<dl>
69-
<dt class="text-base font-normal text-gray-500 dark:text-gray-400 pb-1">Unlisted price</dt>
69+
<dt class="text-base font-normal text-gray-500 dark:text-gray-400 pb-1">{{ $t('Unlisted price') }}</dt>
7070
<dd class="leading-none text-xl font-bold text-red-600 dark:text-red-500">{{
7171
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(
7272
data.totalUnlistedPrice,
@@ -83,7 +83,7 @@ Create a Vue component in the `custom` directory of your project, e.g. `Dashboar
8383
<div class="flex justify-between mb-5">
8484
<div>
8585
<p class="text-base font-normal text-gray-500 dark:text-gray-400">
86-
Unlisted vs Listed price
86+
{{ $t('Unlisted vs Listed price') }}
8787
</p>
8888
</div>
8989
</div>

adminforth/documentation/docs/tutorial/03-Customization/10-menuConfiguration.md

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,11 @@ You can add a badge near the menu item title (e.g. to get count of unread messag
151151
label: 'Posts',
152152
icon: 'flowbite:book-open-outline',
153153
resourceId: 'posts',
154-
badge: async (adminUser: AdminUser) => {
154+
badge: async (adminUser: AdminUser) => {
155155
return 10
156156
},
157-
...,
157+
badgeTooltip: 'New posts', // explain user what this badge means
158+
...
158159
},
159160
],
160161
...
@@ -163,8 +164,64 @@ You can add a badge near the menu item title (e.g. to get count of unread messag
163164

164165
Badge function is async, but all badges are loaded in "lazy" to not block the menu rendering.
165166

166-
To refresh all badges in menu from the frontend component you can use the following code:
167+
### Refreshing the badges
167168

169+
170+
Most times you need to refresh the badge from some backend API or hook. To do this you can do next:
171+
172+
1) Add `itemId` to menu item to identify it:
173+
174+
```ts title='./index.ts'
175+
{
176+
...
177+
menu: [
178+
{
179+
label: 'Posts',
180+
icon: 'flowbite:book-open-outline',
181+
resourceId: 'posts',
182+
//diff-add
183+
itemId: 'postsMenuItem',
184+
badge: async (adminUser: AdminUser) => {
185+
return 10
186+
},
187+
badgeTooltip: 'Unverified posts', // explain user what this badge means
188+
...
189+
},
190+
],
191+
...
192+
}
168193
```
194+
195+
2) On backend point where you need to refresh the badge, you can publish a message to the websocket topic:
196+
197+
```ts title='./index.ts'
198+
{
199+
resourceId: 'posts',
200+
table: 'posts',
201+
hooks: {
202+
edit: {
203+
//diff-add
204+
afterSave: async ({ record, adminUser, resource, adminforth }) => {
205+
//diff-add
206+
const newCount = await adminforth.resource('posts').count(Filters.EQ('verified', false));
207+
//diff-add
208+
adminforth.websocket.publish(`/opentopic/update-menu-badge/postsMenuItem`, { badge: newCount });
209+
//diff-add
210+
return { ok: true }
211+
//diff-add
212+
}
213+
}
214+
}
215+
}
216+
```
217+
218+
219+
> 👆 Please note that any `/opentopic/` publish can be listened by anyone without authorization. If count published in this channel might be
220+
> a subject of security or privacy concerns, you should add [publish authorization](/docs/tutorial/Customization/websocket/#publish-authorization) to the topic.
221+
222+
More rare case when you need to refresh menu item from the frontend component. You can achieve this by calling the next method:
223+
224+
```typescript
169225
window.adminforth.menu.refreshMenuBadges()
170-
```
226+
```
227+

adminforth/documentation/docs/tutorial/03-Customization/15-websocket.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,13 @@ const admin = new AdminForth({
162162

163163
```
164164

165-
### Authorization
165+
### Subscribing authorization
166166

167-
Currently, any user can subscribe to the any topic. Though topic already has user id in it, we should explicitly check that user can subscribe to his own topic using `config.auth.websocketTopicAuth`
167+
Currently, any user can subscribe to the any topic and receive published messages.
168+
169+
However you can prevent some users from subscribing to some topics and prevent them to get data streamed to the topic. Or vise-versa you can prevent all users and allow only some users to subscribe to the topic.
170+
171+
In our example though topic already has user id in it, we should explicitly check that user can subscribe to his own topic using `config.auth.websocketTopicAuth`
168172

169173

170174
```javascript title="./index.ts"
@@ -203,4 +207,34 @@ const admin = new AdminForth({
203207
...
204208
});
205209

206-
```
210+
```
211+
212+
There is still method to bypass this websocketTopicAuth check by using special topic `/opentopic/`. In other words if topic starts with `/opentopic/` it will be allowed to subscribe by any user bypassing `websocketTopicAuth` call at all.
213+
214+
### Publish authorization
215+
216+
Best way to secure the data published to websoket is use websocketTopicAuth method. It will be called once on subscription and if it will not allow access it will completely prevent user from subscribing to the topic.
217+
218+
However you can move auth check to publish call. It has a third parameter of publish function. Imagine you rewrite `websocketTopicAuth` to allow all users to subscribe to any topic:
219+
220+
```typescript title="./index.ts"
221+
...
222+
websocketTopicAuth: async (topic: string, adminUser: AdminUser) => {
223+
return true;
224+
},
225+
...
226+
```
227+
228+
(Or you are using `/opentopic/`)
229+
230+
Now you can move the check to publish call:
231+
232+
```typescript title="./index.ts"
233+
234+
admin.websocket.publish(topic, { type: 'message', totalCost }, async (adminUser: AdminUser): Promise<boolean> => {
235+
return adminUser.dbUser.id === record.realtor_id;
236+
});
237+
238+
```
239+
240+
In this case during publish call it will check all users who subscribed to the topic and do actual publish to only those who are allowed to receive the message. This method requires more CPU resources and generally is not recommended.

adminforth/documentation/docs/tutorial/05-Plugins/07-email-password-reset.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ Plugin allows to reset password for admin users who forgot their password by sen
1111
Installation:
1212

1313
```bash
14-
npm install @adminforth/email-password-reset
15-
npm install @adminforth/email-adapter-aws-ses
14+
npm install @adminforth/email-password-reset --save
15+
npm install @adminforth/email-adapter-aws-ses --save
1616
```
1717

1818
Import plugin:

adminforth/documentation/docs/tutorial/05-Plugins/08-import-export.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ This plugin is mostly useful for next use cases:
1313
To install the plugin:
1414

1515
```bash
16-
npm install @adminforth/import-export
16+
npm install @adminforth/import-export --save
1717
```
1818

1919
## Usage

adminforth/documentation/docs/tutorial/05-Plugins/09-open-signup.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This is useful when you want to allow anyone to sign up and assign some low-leve
88
To install the plugin:
99

1010
```bash
11-
npm install @adminforth/open-signup
11+
npm install @adminforth/open-signup --save
1212
```
1313

1414

adminforth/documentation/docs/tutorial/05-Plugins/10-i18n.md

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Internationalization (i18n)
22

3-
This plugin allows you to translate your AdminForth application to multiple languages.
3+
This plugin allows you translate your AdminForth application to multiple languages.
44
Main features:
5-
- Stores all translation strings in your application in a single resource. Basically you can set allowed actions to Developers/Translators role only if you don't want other users to see the translations.
6-
- Supports AI completion adapters to help with translations. For example, you can use OpenAI ChatGPT to generate translations.
5+
- Stores all translation strings in your application in a single AdminForth resource. You can set [allowed actions](/docs/tutorial/Customization/limitingAccess/#disable-some-action-based-on-logged-in-user-record-or-role) only to Developers/Translators role if you don't want other users to see/edit the translations.
6+
- Supports AI completion adapters to help with translations. For example, you can use OpenAI ChatGPT to generate translations. Supports correct pluralization, even for Slavic languages.
77
- Supports any number of languages.
88

99

@@ -15,7 +15,8 @@ Under the hood it uses vue-i18n library and provides several additional faciliti
1515
To install the plugin:
1616

1717
```bash
18-
npm install @adminforth/i18n
18+
npm install @adminforth/i18n --save
19+
npm install @adminforth/completion-adapter-open-ai-chat-gpt --save
1920
```
2021

2122
For example lets add translations to next 4 languages: Ukrainian, Japanese, French, Spanish. Also we will support basic translation for English.
@@ -86,6 +87,11 @@ export default {
8687

8788
completeAdapter: new CompletionAdapterOpenAIChatGPT({
8889
openAiApiKey: process.env.OPENAI_API_KEY as string,
90+
model: 'gpt-4o-mini',
91+
expert: {
92+
// for UI translation it is better to lower down the temperature from default 0.7. Less creative and more accurate
93+
temperature: 0.5,
94+
},
8995
}),
9096
}),
9197

@@ -137,6 +143,12 @@ export default {
137143
} as AdminForthResourceInput;
138144
```
139145
146+
Add `OPENAI_API_KEY` to your `.env` file:
147+
148+
```bash
149+
OPENAI_API_KEY=your_openai_api_key
150+
```
151+
140152
Also add the resource to main file and add menu item in `./index.ts`:
141153
142154
```ts title='./index.ts'
@@ -169,7 +181,7 @@ const adminForth = new AdminForth({
169181

170182
```
171183
172-
This is it, now you should start your app and see the translations resource in the menu.
184+
This is it, now you should restart your app and see the translations resource in the menu.
173185
174186
You can add translations for each language manually or use Bulk actions to generate translations with AI completion adapter.
175187
@@ -202,13 +214,13 @@ This is generally helps to understand the context of the translation for AI comp
202214
For example if you have string "Showing 1 to 10 of 100 entries" you can of course simply do
203215
204216
```html
205-
{{$t('Showing')} {{from}} {{$t('to')}} {{to}} {{$t('of')}} {{total}} {{$t('entries')}}
206-
```
217+
{{ $t('Showing')}} {{from}} {{$t('to')}} {{to}} {{$t('of')}} {{total}} {{$t('entries') }}
218+
```
207219
208220
And it will form 4 translation strings. But it is much better to have it as single string with variables like this:
209221
210222
```html
211-
{{$t('Showing {from} to {to} of {total} entries', { from, to, total })}
223+
{{ $t('Showing {from} to {to} of {total} entries', { from, to, total } ) }}
212224
```
213225
214226
@@ -254,6 +266,30 @@ const adminForth = new AdminForth({
254266

255267
```
256268
269+
### HTML in translations
270+
271+
Sometimes you might want to have HTML in translations. You can use `v-html` directive for this. For example:
272+
273+
```html
274+
<h1 class="mb-4 text-xl font-extrabold text-gray-900 dark:text-white md:text-2xl lg:text-3xl"
275+
v-html='$t("<span class=\"text-transparent bg-clip-text bg-gradient-to-r to-emerald-600 from-sky-400\">Apartments</span> Statistics.")'></h1>
276+
```
277+
278+
> 🪪 Please keep in mind that roles who have access to translations resource can make HTML injections, though anyway if you are using [Audit Log](/docs/tutorial/Plugins/AuditLog/) plugin you can track all changes in translations and understand who made them.
279+
280+
### Pluralization
281+
282+
Frontend uses same pluralization rules as vue-i18n library. You can use it in the same way. For example:
283+
284+
```html
285+
{{ $t('Apartment last 7 days | Apartments last 7 days', data.totalAparts) }}
286+
```
287+
288+
For English it will use 2 pluralization forms (1 and other), for Slavic languages, LLM adapter will be instructed to generate 4 forms: for zero, for one, for 2-4 and for 5+:
289+
290+
![alt text](image-4.png)
291+
292+
257293
## Translations in custom APIs
258294
259295
Sometimes you need to return a translated error or success message from your API. You can use special `tr` function for this.
@@ -328,7 +364,7 @@ If you don't use params, you can use `tr` without third param:
328364
> So to collect all translations you should use your app for some time and make sure all strings are used at
329365
> In future we plan to add backend strings collection in same way like frontend strings are collected.
330366
331-
# Translating messaged within bulk action
367+
## Translating messaged within bulk action
332368
333369
Label adn confirm strings of bulk actions are already translated by AdminForth, but
334370
`succesMessage` returned by action function should be translated manually because of the dynamic nature of the message.
129 KB
Loading

0 commit comments

Comments
 (0)