You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: content-translator/CHANGELOG.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,6 +2,8 @@
2
2
3
3
## Unreleased
4
4
5
+
- feat: add a per-field `custom['content-translator']` config (typed via module augmentation) with orthogonal `skip`, `beforeTranslate`, and `afterTranslate` hooks, so a slug can either be derived from the translated title (skip + derive) or translated and then slugified (translate + normalize)
6
+
-**BREAKING**: the `custom.translatorSkip` flag is removed — move it to `custom: { 'content-translator': { skip: true } }`
5
7
-**BREAKING**: serve the translate endpoint at `/api/content-translator/translate` (previously `/api/translator/translate`) so the endpoint prefix matches the plugin slug. Any API client calling the old path must be updated.
|`skip`|`boolean`| Exclude the field from the resolver — its value is not translated. Use alone to let the app own it, or with `afterTranslate`. |
68
+
|`beforeTranslate`|`(args) => string`| Transform the source string right before it is sent to the resolver. The translated result is written back as usual. |
69
+
|`afterTranslate`|`(args) => unknown \| Promise<...>`| Post-process the field _after_ the rest of the document is translated. Runs independently of `skip`. |
70
+
71
+
The three keys are orthogonal: `skip` decides whether the field is translated,
72
+
`beforeTranslate` pre-processes what is sent, and `afterTranslate` post-processes
73
+
the result (or derives a value from translated siblings).
74
+
75
+
#### Translating slug fields
76
+
77
+
A slug must never be stored with the raw output of an LLM — it has to stay
78
+
URL-safe. There are two strategies, depending on whether the slug should mirror
79
+
the title:
80
+
81
+
**Derive from the title** — the slug always follows the title, so skip
82
+
translation and re-slugify the already-translated title (e.g. "Travel Tips" →
**Translate, then normalize** — for a slug intentionally different from the
101
+
title, translate the slug text and then slugify it to strip any special
102
+
characters the translation introduced:
103
+
104
+
```ts
105
+
{
106
+
name: 'slug',
107
+
type: 'text',
108
+
localized: true,
109
+
custom: {
110
+
'content-translator': {
111
+
// No `skip`: the slug is translated, then cleaned.
112
+
afterTranslate: ({ value }) =>slugify(value),
113
+
},
114
+
},
115
+
// validation and other field options...
116
+
}
117
+
```
118
+
119
+
`afterTranslate` receives the field's own (translated) `value`, the translated
120
+
`siblingData`, the full translated `data`, `sourceValue`, `localeFrom`/`localeTo`,
121
+
and `req`. Because it is field-local, this works for any derived or normalized
122
+
field under any name — slugs, URL paths, computed keys.
123
+
124
+
#### With the Pages plugin
125
+
126
+
The [Pages plugin](https://www.npmjs.com/package/@jhb.software/payload-pages-plugin)
127
+
injects the `slug` field into its page collections, so you attach the config with
128
+
a small plugin that runs after `payloadPagesPlugin` and derives the slug from the
129
+
translated title — normalized with the pages plugin's `formatSlug`, the same rule
130
+
the slug field validates against, so the result is always accepted.
131
+
132
+
See the runnable example: [`makeSlugTranslatable`](https://github.com/jhb-software/payload-plugins/blob/main/content-translator/dev/src/helpers/makeSlugTranslatable.ts)
133
+
and [its wiring](https://github.com/jhb-software/payload-plugins/blob/main/content-translator/dev/src/payload.config.ts) in the dev app.
134
+
57
135
### Resolvers
58
136
59
137
This plugin is designed to work seamlessly with various translation services by accepting a customizable translation resolver as a configuration option.
@@ -75,9 +153,9 @@ openAIResolver({
75
153
76
154
The plugin registers a single REST endpoint that the admin UI calls to translate a document or global. It computes the translated values and returns them in the response — it never writes to the database, so changes still go through Payload's normal save/publish flow.
0 commit comments