Skip to content

Commit 976be8f

Browse files
committed
dynamic translation post
1 parent e296fe7 commit 976be8f

File tree

1 file changed

+274
-0
lines changed
  • adminforth/documentation/blog/2024-04-10-how-to-translate dynamic-strings

1 file changed

+274
-0
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
---
2+
slug: dynamic-strings-translation
3+
title: "How to translate dynamic strings in AdminForth API"
4+
authors: ivanb
5+
tags: [keycloak, authentication]
6+
description: "Simple example of how to translate dynamic strings from database in AdminForth API"
7+
# image: "/ogs/ga-tf-aws.jpg"
8+
---
9+
10+
When you are using [AdminForth i18n plugin for external Apps translation](https://adminforth.dev/docs/tutorial/Plugins/i18n/#translating-external-application) you might face a case when you need to translate some data stored in your database which potentially can be changed in future.
11+
<!-- truncate -->
12+
13+
Let's consider simple example where we have a Page resource
14+
15+
```ts
16+
import { AdminForthResourceInput } from "adminforth";
17+
18+
export default {
19+
dataSource: "maindb",
20+
table: "pages",
21+
resourceId: "pages",
22+
label: "Pages",
23+
columns: [
24+
{
25+
name: "url",
26+
primaryKey: true,
27+
showIn: { all: false },
28+
},
29+
{
30+
name: "meta_title",
31+
label: "Meta Title",
32+
type: "string",
33+
showIn: { all: true },
34+
},
35+
{
36+
name: "meta_desc",
37+
label: "Meta Description",
38+
type: "string",
39+
showIn: { all: true },
40+
},
41+
]
42+
} as AdminForthResourceInput;
43+
```
44+
45+
You might have this page and return it in your API for nuxt:
46+
47+
```ts
48+
app.get(`${admin.config.baseUrl}/api/get_page`,
49+
async (req:any, res: Response): Promise<void> => {
50+
const pageUrl = req.query.pageUrl;
51+
if (!pageUrl) {
52+
res.status(400).json({ error: "pageUrl is required" });
53+
return;
54+
}
55+
const page = await admin.resource("pages").get([Filters.EQ("url", pageUrl)]);
56+
if (!page) {
57+
res.status(404).json({ error: `Page not found ${pageUrl}` });
58+
return;
59+
}
60+
res.json({
61+
meta_title: page.meta_title,
62+
meta_desc: page.meta_desc,
63+
});
64+
}
65+
)
66+
);
67+
```
68+
69+
Now you want to translate page meta title and meta description. You can do this by using `i18n` plugin for AdminForth.
70+
71+
```ts
72+
import { AdminForth } from "adminforth";
73+
74+
export const SEO_PAGE_CATEGORY = "seo_page_config";
75+
76+
app.get(`${admin.config.baseUrl}/api/get_page`,\
77+
//diff-add
78+
admin.express.translatable(
79+
async (req:any, res: Response): Promise<void> => {
80+
const pageUrl = req.query.pageUrl;
81+
if (!pageUrl) {
82+
res.status(400).json({ error: "pageUrl is required" });
83+
return;
84+
}
85+
const page = await admin.resource("pages").get([Filters.EQ("url", pageUrl)]);
86+
if (!page) {
87+
res.status(404).json({ error: `Page not found ${pageUrl}` });
88+
return;
89+
}
90+
91+
//diff-add
92+
const translateKeys = [ "meta_title", "meta_desc", ];
93+
//diff-add
94+
const [meta_title, meta_desc] =
95+
//diff-add
96+
await Promise.all(translateKeys.map((key: string) => req.tr(page[key], SEO_PAGE_CATEGORY)));
97+
98+
99+
res.json({
100+
//diff-remove
101+
meta_title: page.meta_title,
102+
//diff-add
103+
meta_title,
104+
//diff-remove
105+
meta_desc: page.meta_desc,
106+
//diff-add
107+
meta_desc,
108+
});
109+
}
110+
//diff-add
111+
)
112+
);
113+
```
114+
115+
Looks straightforward, but here are 2 issues:
116+
117+
# Issue one - you need to call `req.tr` for each string before translating it in Admin
118+
119+
Since translation strings are created only when first time you call `req.tr` function, you need to ensure you will call this API for all your pages (in all envs e.g. local, dev, staging, prod ), this might be not convinient - you have to call this API, then go to translation page and translate it with bulk action LLM or manually, but you might forget one page or so.
120+
121+
To fix this we suggest next, go to Page resource and add next function on top level:
122+
123+
```ts
124+
//diff-remove
125+
import { AdminForthResourceInput } from "adminforth";
126+
//diff-add
127+
import AdminForth, { AdminForthResourceInput, Filters, IAdminForth } from "adminforth";
128+
//diff-add
129+
import I18nPlugin from "@adminforth/i18n/index.js";
130+
131+
//diff-add
132+
import { SEO_PAGE_CATEGORY } from "../api.ts";
133+
134+
//diff-add
135+
export async function feedAllPageTranslations(adminforth: IAdminForth) {
136+
//diff-add
137+
const i18nPlugin = adminforth.getPluginByClassName<I18nPlugin>('I18nPlugin');
138+
//diff-add
139+
const pages = await adminforth.resource('pages').list([]);
140+
//diff-add
141+
await Promise.all(
142+
//diff-add
143+
pages.map(async (page: any) => {
144+
//diff-add
145+
await Promise.all(
146+
//diff-add
147+
['meta_title', 'meta_desc'].map(async (key) => {
148+
//diff-add
149+
if (page[key]) {
150+
//diff-add
151+
await i18nPlugin.feedCategoryTranslations(
152+
//diff-add
153+
[{ en_string: page[key], source: `pages.${page.url}.${key}` }], SEO_PAGE_CATEGORY
154+
//diff-add
155+
);
156+
//diff-add
157+
}
158+
//diff-add
159+
})
160+
//diff-add
161+
);
162+
//diff-add
163+
})
164+
//diff-add
165+
);
166+
//diff-add
167+
}
168+
169+
export default {
170+
dataSource: "maindb",
171+
table: "pages",
172+
resourceId: "pages",
173+
...
174+
```
175+
176+
This function will iterate all pages and call `feedCategoryTranslations` function for each page and each key. This will create translation strings in your database for each page and each key. If translation objects already exist, it will skip them (**will not** create duplicates and **will not** overwrite translations).
177+
178+
Now we need to call this function in hooks:
179+
180+
```ts
181+
export default {
182+
dataSource: "maindb",
183+
table: "pages",
184+
resourceId: "pages",
185+
label: "Pages",
186+
//diff-add
187+
hooks: {
188+
//diff-add
189+
create: {
190+
//diff-add
191+
afterSave: async ({ record, adminforth }: { record: Record<string, string>, adminforth: IAdminForth }) => {
192+
//diff-add
193+
feedAllPageTranslations(adminforth);
194+
//diff-add
195+
return { ok: true };
196+
//diff-add
197+
},
198+
//diff-add
199+
},
200+
//diff-add
201+
edit: {
202+
//diff-add
203+
afterSave: async ({ oldRecord, updates, adminforth }: { oldRecord: Record<string, string>, updates: Record<string, string>, adminforth: IAdminForth }) => {
204+
//diff-add
205+
feedAllPageTranslations(adminforth);
206+
//diff-add
207+
return { ok: true };
208+
//diff-add
209+
},
210+
//diff-add
211+
},
212+
},
213+
columns: [
214+
...
215+
```
216+
217+
> Please note that we run this function without await, so it will not block your API. SO function will be called in background when hook will already return and user will not wait for it.
218+
219+
220+
Now every time you will create or edit page, it will call `feedAllPageTranslations` function and create translation strings for each page and each key.
221+
You can also import this function into index script and run after database discover, if you already have pages in your database and you want to create translation strings for them even without clicking on create or edit button.
222+
223+
# Issue 2 - after modification of page attributes old translation strings will not be removed
224+
225+
You can mitigate this by adding couple of lines into edit hook:
226+
227+
```ts
228+
edit: {
229+
afterSave: async ({ oldRecord, updates, adminforth }: { oldRecord: Record<string, string>, updates: Record<string, string>, adminforth: IAdminForth }) => {
230+
//diff-add
231+
if (Object.keys(updates).length) {
232+
//diff-add
233+
// find old strings which were edited and which are not used anymore
234+
//diff-add
235+
const oldStrings = await adminforth.resource('translations').list([
236+
//diff-add
237+
Filters.AND([
238+
//diff-add
239+
Filters.EQ('category', 'seo_page_config'),
240+
//diff-add
241+
Filters.IN('en_string', Object.keys(updates).map((key: string) => oldRecord[key]))
242+
//diff-add
243+
])
244+
//diff-add
245+
]);
246+
//diff-add
247+
// delete them
248+
//diff-add
249+
await Promise.all(
250+
//diff-add
251+
oldStrings.map((oldString: any) => {
252+
//diff-add
253+
return adminforth.resource('translations').delete(oldString.id);
254+
//diff-add
255+
})
256+
//diff-add
257+
);
258+
//diff-add
259+
}
260+
feedAllPageTranslations(adminforth);
261+
return { ok: true };
262+
},
263+
},
264+
```
265+
266+
This will delete all old translation strings which are not used anymore.
267+
268+
If your have ability to delete pages, you can also add delete hook, you can do this as a homework.
269+
270+
271+
# Conclusion
272+
273+
In this article we have shown how to translate dynamic strings in AdminForth API. We have also shown how to create translation strings for each page and each key in your database.
274+
We have also shown how to delete old translation strings which are not used anymore. This will help you to keep your translation strings clean and up to date.

0 commit comments

Comments
 (0)