Skip to content

Commit ad8f0b2

Browse files
authored
feat(plugin-redirects): support translations (#14548)
Allows you to add translations to the plugin fields, replacing the need for verbose field `overrides` configuration. ### Usage ```ts export default buildConfig({ i18n: { translations: { en: { 'plugin-redirects': { fromUrl: 'Source URL (Custom)', // Override }, }, de: { 'plugin-redirects': { fromUrl: 'Quell-URL', internalLink: 'Interner Link', // ... other keys }, }, }, }, plugins: [redirectsPlugin({ collections: ['pages'] })], }) ```
1 parent 99ed0e5 commit ad8f0b2

File tree

16 files changed

+482
-42
lines changed

16 files changed

+482
-42
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ jobs:
322322
- plugin-import-export
323323
- plugin-multi-tenant
324324
- plugin-nested-docs
325+
- plugin-redirects
325326
- plugin-seo
326327
- sort
327328
- trash

packages/payload/src/config/sanitize.ts

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -176,35 +176,6 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
176176
i18nConfig.translations
177177
}
178178

179-
// Inject custom translations from i18n.translations into supportedLanguages
180-
// This allows label functions like ({ t }) => t('namespace:key') to work with custom translations
181-
// that users define in their config, not just the default translations from @payloadcms/translations
182-
if (i18nConfig.translations && typeof i18nConfig.translations === 'object') {
183-
Object.keys(i18nConfig.translations).forEach((lang) => {
184-
const langKey = lang as AcceptedLanguages
185-
const customTranslations = i18nConfig.translations[langKey]
186-
187-
if (customTranslations && typeof customTranslations === 'object') {
188-
const existingLang = i18nConfig.supportedLanguages[langKey]
189-
190-
if (existingLang) {
191-
// Language exists - merge custom translations with existing
192-
const merged = deepMergeSimple(existingLang.translations || {}, customTranslations)
193-
// @ts-expect-error - merging custom translations into language config
194-
i18nConfig.supportedLanguages[langKey] = { ...existingLang, translations: merged }
195-
} else {
196-
// Language doesn't exist - create it using 'en' as template
197-
// Merge en.translations (general, authentication, etc.) with custom translations
198-
const mergedTranslations = deepMergeSimple(en.translations, customTranslations)
199-
i18nConfig.supportedLanguages[langKey] = {
200-
...en,
201-
translations: mergedTranslations,
202-
} as Language<typeof en.translations>
203-
}
204-
}
205-
})
206-
}
207-
208179
config.i18n = i18nConfig
209180

210181
const richTextSanitizationPromises: Array<(config: SanitizedConfig) => Promise<void>> = []

packages/plugin-redirects/README.md

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,163 @@
22

33
A plugin for [Payload](https://github.com/payloadcms/payload) to easily manage your redirects from within your admin panel.
44

5+
## Features
6+
7+
- Manage redirects directly from your admin panel
8+
- Support for internal (reference) and external (custom URL) redirects
9+
- Built-in multi-language support
10+
- Optional redirect types (301, 302, etc.)
11+
12+
## Installation
13+
14+
```bash
15+
pnpm add @payloadcms/plugin-redirects
16+
```
17+
18+
## Basic Usage
19+
20+
In your [Payload Config](https://payloadcms.com/docs/configuration/overview), add the plugin:
21+
22+
```ts
23+
import { buildConfig } from 'payload'
24+
import { redirectsPlugin } from '@payloadcms/plugin-redirects'
25+
26+
export default buildConfig({
27+
plugins: [
28+
redirectsPlugin({
29+
collections: ['pages'], // Collections to use in the 'to' relationship field
30+
}),
31+
],
32+
})
33+
```
34+
35+
## Configuration
36+
37+
### Options
38+
39+
| Option | Type | Description |
40+
| --------------------------- | ---------- | ------------------------------------------------------------------------------------------------------- |
41+
| `collections` | `string[]` | An array of collection slugs to populate in the `to` field of each redirect. |
42+
| `overrides` | `object` | A partial collection config that allows you to override anything on the `redirects` collection. |
43+
| `redirectTypes` | `string[]` | Provide an array of redirects if you want to provide options for the type of redirects to be supported. |
44+
| `redirectTypeFieldOverride` | `Field` | A partial Field config that allows you to override the Redirect Type field if enabled above. |
45+
46+
### Advanced Example
47+
48+
```ts
49+
import { buildConfig } from 'payload'
50+
import { redirectsPlugin } from '@payloadcms/plugin-redirects'
51+
52+
export default buildConfig({
53+
plugins: [
54+
redirectsPlugin({
55+
collections: ['pages', 'posts'],
56+
57+
// Add custom redirect types
58+
redirectTypes: ['301', '302'],
59+
60+
// Override the redirects collection
61+
overrides: {
62+
slug: 'custom-redirects',
63+
64+
// Add custom fields
65+
fields: ({ defaultFields }) => {
66+
return [
67+
...defaultFields,
68+
{
69+
name: 'notes',
70+
type: 'textarea',
71+
admin: {
72+
description: 'Internal notes about this redirect',
73+
},
74+
},
75+
]
76+
},
77+
},
78+
}),
79+
],
80+
})
81+
```
82+
83+
## Custom Translations
84+
85+
The plugin automatically includes translations for English, French, and Spanish. If you want to customize existing translations or add new languages, you can override them in your config:
86+
87+
```ts
88+
import { buildConfig } from 'payload'
89+
import { redirectsPlugin } from '@payloadcms/plugin-redirects'
90+
91+
export default buildConfig({
92+
i18n: {
93+
translations: {
94+
// Add your custom language
95+
de: {
96+
'plugin-redirects': {
97+
fromUrl: 'Von URL',
98+
toUrlType: 'Ziel-URL-Typ',
99+
internalLink: 'Interner Link',
100+
customUrl: 'Benutzerdefinierte URL',
101+
documentToRedirect: 'Dokument zum Weiterleiten',
102+
redirectType: 'Weiterleitungstyp',
103+
},
104+
},
105+
// Or override existing translations
106+
fr: {
107+
'plugin-redirects': {
108+
fromUrl: 'URL source', // Custom override
109+
},
110+
},
111+
},
112+
},
113+
114+
plugins: [
115+
redirectsPlugin({
116+
collections: ['pages'],
117+
}),
118+
],
119+
})
120+
```
121+
122+
## Using Redirects in Your Frontend
123+
124+
The plugin creates a `redirects` collection in your database. You can query this collection from your frontend and implement the redirects using your framework's routing system.
125+
126+
### Example: Next.js Middleware
127+
128+
```ts
129+
// middleware.ts
130+
import { NextResponse } from 'next/server'
131+
import type { NextRequest } from 'next/server'
132+
133+
export async function middleware(request: NextRequest) {
134+
const { pathname } = request.nextUrl
135+
136+
// Fetch redirects from Payload API
137+
const redirects = await fetch('http://localhost:3000/api/redirects', {
138+
next: { revalidate: 60 }, // Cache for 60 seconds
139+
}).then((res) => res.json())
140+
141+
// Find matching redirect
142+
const redirect = redirects.docs?.find((r: any) => r.from === pathname)
143+
144+
if (redirect) {
145+
const destination =
146+
redirect.to.type === 'reference'
147+
? redirect.to.reference.slug // Adjust based on your collection structure
148+
: redirect.to.url
149+
150+
return NextResponse.redirect(
151+
new URL(destination, request.url),
152+
redirect.type === '301' ? 301 : 302,
153+
)
154+
}
155+
156+
return NextResponse.next()
157+
}
158+
```
159+
160+
## Links
161+
5162
- [Source code](https://github.com/payloadcms/payload/tree/main/packages/plugin-redirects)
6163
- [Documentation](https://payloadcms.com/docs/plugins/redirects)
7-
- [Documentation source](https://github.com/payloadcms/payload/tree/main/docs/plugins/redirects.mdx)
164+
- [Issue tracker](https://github.com/payloadcms/payload/issues)

packages/plugin-redirects/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,13 @@
5656
"lint:fix": "eslint . --fix",
5757
"prepublishOnly": "pnpm clean && pnpm turbo build"
5858
},
59-
"devDependencies": {
60-
"@payloadcms/eslint-config": "workspace:*",
59+
"dependencies": {
60+
"@payloadcms/translations": "workspace:*",
6161
"payload": "workspace:*"
6262
},
63+
"devDependencies": {
64+
"@payloadcms/eslint-config": "workspace:*"
65+
},
6366
"peerDependencies": {
6467
"payload": "workspace:*"
6568
},
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export type { RedirectsTranslationKeys, RedirectsTranslations } from '../translations/index.js'
12
export type { RedirectsPluginConfig } from '../types.js'

packages/plugin-redirects/src/index.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
11
import type { CollectionConfig, Config, Field, SelectField } from 'payload'
22

3+
import { deepMergeSimple } from 'payload/shared'
4+
35
import type { RedirectsPluginConfig } from './types.js'
46

57
import { redirectOptions } from './redirectTypes.js'
8+
import { translations } from './translations/index.js'
69

710
export { redirectOptions, redirectTypes } from './redirectTypes.js'
11+
export { translations as redirectsTranslations } from './translations/index.js'
812
export const redirectsPlugin =
913
(pluginConfig: RedirectsPluginConfig) =>
1014
(incomingConfig: Config): Config => {
15+
// Merge translations FIRST (before building fields)
16+
if (!incomingConfig.i18n) {
17+
incomingConfig.i18n = {}
18+
}
19+
20+
if (!incomingConfig.i18n?.translations) {
21+
incomingConfig.i18n.translations = {}
22+
}
23+
24+
incomingConfig.i18n.translations = deepMergeSimple(
25+
translations,
26+
incomingConfig.i18n?.translations,
27+
)
28+
1129
const redirectSelectField: SelectField = {
1230
name: 'type',
1331
type: 'select',
14-
label: 'Redirect Type',
32+
// @ts-expect-error - translations are not typed in plugins yet
33+
label: ({ t }) => t('plugin-redirects:redirectType'),
1534
options: redirectOptions.filter((option) =>
1635
pluginConfig?.redirectTypes?.includes(option.value),
1736
),
@@ -26,7 +45,8 @@ export const redirectsPlugin =
2645
name: 'from',
2746
type: 'text',
2847
index: true,
29-
label: 'From URL',
48+
// @ts-expect-error - translations are not typed in plugins yet
49+
label: ({ t }) => t('plugin-redirects:fromUrl'),
3050
required: true,
3151
unique: true,
3252
},
@@ -41,14 +61,17 @@ export const redirectsPlugin =
4161
layout: 'horizontal',
4262
},
4363
defaultValue: 'reference',
44-
label: 'To URL Type',
64+
// @ts-expect-error - translations are not typed in plugins yet
65+
label: ({ t }) => t('plugin-redirects:toUrlType'),
4566
options: [
4667
{
47-
label: 'Internal link',
68+
// @ts-expect-error - translations are not typed in plugins yet
69+
label: ({ t }) => t('plugin-redirects:internalLink'),
4870
value: 'reference',
4971
},
5072
{
51-
label: 'Custom URL',
73+
// @ts-expect-error - translations are not typed in plugins yet
74+
label: ({ t }) => t('plugin-redirects:customUrl'),
5275
value: 'custom',
5376
},
5477
],
@@ -59,7 +82,8 @@ export const redirectsPlugin =
5982
admin: {
6083
condition: (_, siblingData) => siblingData?.type === 'reference',
6184
},
62-
label: 'Document to redirect to',
85+
// @ts-expect-error - translations are not typed in plugins yet
86+
label: ({ t }) => t('plugin-redirects:documentToRedirect'),
6387
relationTo: pluginConfig?.collections || [],
6488
required: true,
6589
},
@@ -69,7 +93,8 @@ export const redirectsPlugin =
6993
admin: {
7094
condition: (_, siblingData) => siblingData?.type === 'custom',
7195
},
72-
label: 'Custom URL',
96+
// @ts-expect-error - translations are not typed in plugins yet
97+
label: ({ t }) => t('plugin-redirects:customUrl'),
7398
required: true,
7499
},
75100
],
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { GenericTranslationsObject, NestedKeysStripped } from '@payloadcms/translations'
2+
3+
import { en } from './languages/en.js'
4+
import { es } from './languages/es.js'
5+
import { fr } from './languages/fr.js'
6+
7+
export const translations = {
8+
en,
9+
es,
10+
fr,
11+
}
12+
13+
export type RedirectsTranslations = GenericTranslationsObject
14+
15+
export type RedirectsTranslationKeys = NestedKeysStripped<RedirectsTranslations>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { GenericTranslationsObject } from '@payloadcms/translations'
2+
3+
export const en: GenericTranslationsObject = {
4+
$schema: '../translation-schema.json',
5+
'plugin-redirects': {
6+
customUrl: 'Custom URL',
7+
documentToRedirect: 'Document to redirect to',
8+
fromUrl: 'From URL',
9+
internalLink: 'Internal link',
10+
redirectType: 'Redirect Type',
11+
toUrlType: 'To URL Type',
12+
},
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { GenericTranslationsObject } from '@payloadcms/translations'
2+
3+
export const es: GenericTranslationsObject = {
4+
$schema: '../translation-schema.json',
5+
'plugin-redirects': {
6+
customUrl: 'URL personalizada',
7+
documentToRedirect: 'Documento al que redirigir',
8+
fromUrl: 'URL de origen',
9+
internalLink: 'Enlace interno',
10+
redirectType: 'Tipo de redirección',
11+
toUrlType: 'Tipo de URL de destino',
12+
},
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { GenericTranslationsObject } from '@payloadcms/translations'
2+
3+
export const fr: GenericTranslationsObject = {
4+
$schema: '../translation-schema.json',
5+
'plugin-redirects': {
6+
customUrl: 'URL personnalisée',
7+
documentToRedirect: 'Page de redirection',
8+
fromUrl: "URL d'origine",
9+
internalLink: 'Référence vers une page',
10+
redirectType: 'Type de redirection',
11+
toUrlType: "Type d'URL de destination",
12+
},
13+
}

0 commit comments

Comments
 (0)