@roxxel/payload-multilang 0.0.1

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.
Files changed (62) hide show
  1. package/README.md +165 -0
  2. package/dist/components/LanguageListToolbar.d.ts +6 -0
  3. package/dist/components/LanguageListToolbar.js +69 -0
  4. package/dist/components/LanguageListToolbar.js.map +1 -0
  5. package/dist/components/LanguageMetabox.d.ts +2 -0
  6. package/dist/components/LanguageMetabox.js +275 -0
  7. package/dist/components/LanguageMetabox.js.map +1 -0
  8. package/dist/components/TranslationActionsClient.d.ts +10 -0
  9. package/dist/components/TranslationActionsClient.js +166 -0
  10. package/dist/components/TranslationActionsClient.js.map +1 -0
  11. package/dist/components/TranslationColumnCell.d.ts +2 -0
  12. package/dist/components/TranslationColumnCell.js +69 -0
  13. package/dist/components/TranslationColumnCell.js.map +1 -0
  14. package/dist/components/TranslationColumnCellClient.d.ts +12 -0
  15. package/dist/components/TranslationColumnCellClient.js +107 -0
  16. package/dist/components/TranslationColumnCellClient.js.map +1 -0
  17. package/dist/components/TranslationsTab.d.ts +2 -0
  18. package/dist/components/TranslationsTab.js +118 -0
  19. package/dist/components/TranslationsTab.js.map +1 -0
  20. package/dist/components/config.d.ts +40 -0
  21. package/dist/components/config.js +31 -0
  22. package/dist/components/config.js.map +1 -0
  23. package/dist/constants.d.ts +5 -0
  24. package/dist/constants.js +24 -0
  25. package/dist/constants.js.map +1 -0
  26. package/dist/endpoints/translations.d.ts +19 -0
  27. package/dist/endpoints/translations.js +301 -0
  28. package/dist/endpoints/translations.js.map +1 -0
  29. package/dist/exports/client.d.ts +4 -0
  30. package/dist/exports/client.js +6 -0
  31. package/dist/exports/client.js.map +1 -0
  32. package/dist/exports/rsc.d.ts +2 -0
  33. package/dist/exports/rsc.js +4 -0
  34. package/dist/exports/rsc.js.map +1 -0
  35. package/dist/hooks/translatedCollection.d.ts +26 -0
  36. package/dist/hooks/translatedCollection.js +290 -0
  37. package/dist/hooks/translatedCollection.js.map +1 -0
  38. package/dist/hooks/translatedGlobal.d.ts +16 -0
  39. package/dist/hooks/translatedGlobal.js +71 -0
  40. package/dist/hooks/translatedGlobal.js.map +1 -0
  41. package/dist/index.d.ts +5 -0
  42. package/dist/index.js +63 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/lib/config.d.ts +14 -0
  45. package/dist/lib/config.js +96 -0
  46. package/dist/lib/config.js.map +1 -0
  47. package/dist/lib/data.d.ts +107 -0
  48. package/dist/lib/data.js +307 -0
  49. package/dist/lib/data.js.map +1 -0
  50. package/dist/payload-config.d.js +2 -0
  51. package/dist/payload-config.d.js.map +1 -0
  52. package/dist/styles/admin.css +316 -0
  53. package/dist/types.d.ts +96 -0
  54. package/dist/types.js +3 -0
  55. package/dist/types.js.map +1 -0
  56. package/docs/assets/admin-ui/collection-list-language-shortcuts.png +0 -0
  57. package/docs/assets/admin-ui/english-post-translations.png +0 -0
  58. package/docs/assets/admin-ui/ukrainian-post-translations.png +0 -0
  59. package/docs/configuration.md +192 -0
  60. package/docs/helpers.md +231 -0
  61. package/docs/usage.md +269 -0
  62. package/package.json +95 -0
@@ -0,0 +1,231 @@
1
+ # Helper API
2
+
3
+ Import helpers from `@roxxel/payload-multilang`.
4
+
5
+ ```ts
6
+ import {
7
+ findGlobalByLanguage,
8
+ findGlobalByLanguageWithPayload,
9
+ getDocumentTranslation,
10
+ getDocumentTranslations,
11
+ getLanguages,
12
+ updateGlobalByLanguage,
13
+ updateGlobalByLanguageWithPayload,
14
+ withLanguage,
15
+ } from '@roxxel/payload-multilang'
16
+ ```
17
+
18
+ ## `getLanguages`
19
+
20
+ Returns the languages configured for the plugin.
21
+
22
+ ```ts
23
+ const languages = await getLanguages()
24
+ ```
25
+
26
+ You can also pass known languages or a Payload config-like object.
27
+
28
+ ```ts
29
+ const languages = await getLanguages({
30
+ config: payload.config,
31
+ })
32
+ ```
33
+
34
+ ## `withLanguage`
35
+
36
+ Adds a language constraint to a Payload `where` query.
37
+
38
+ ```ts
39
+ const where = withLanguage({
40
+ language: 'uk',
41
+ where: {
42
+ title: {
43
+ like: 'Hello',
44
+ },
45
+ },
46
+ })
47
+ ```
48
+
49
+ Result:
50
+
51
+ ```ts
52
+ {
53
+ and: [
54
+ {
55
+ title: {
56
+ like: 'Hello',
57
+ },
58
+ },
59
+ {
60
+ _multilangLanguage: {
61
+ equals: 'uk',
62
+ },
63
+ },
64
+ ],
65
+ }
66
+ ```
67
+
68
+ Signature:
69
+
70
+ ```ts
71
+ withLanguage(args: {
72
+ fieldName?: string
73
+ language: string
74
+ where?: Where
75
+ }): Where
76
+ ```
77
+
78
+ Use `fieldName` if you configured a custom collection language field.
79
+
80
+ ## `getDocumentTranslations`
81
+
82
+ Returns the source document and all linked translations for a localized collection document.
83
+
84
+ ```ts
85
+ const state = await getDocumentTranslations<Post>({
86
+ collection: 'posts',
87
+ id: post.id,
88
+ })
89
+ ```
90
+
91
+ Signature:
92
+
93
+ ```ts
94
+ getDocumentTranslations<TDoc>(args: {
95
+ collection: CollectionSlug
96
+ fieldNames?: MultilangFieldNames
97
+ id: number | string
98
+ }): Promise<TranslationState<TDoc>>
99
+ ```
100
+
101
+ Return shape:
102
+
103
+ ```ts
104
+ type TranslationState<TDoc = Record<string, unknown>> = {
105
+ group?: string
106
+ language?: string
107
+ source?: TDoc
108
+ translations: Record<string, TDoc>
109
+ }
110
+ ```
111
+
112
+ ## `getDocumentTranslation`
113
+
114
+ Returns one target-language document from the source document's translation group.
115
+
116
+ ```ts
117
+ const ukrainianPost = await getDocumentTranslation<Post>({
118
+ collection: 'posts',
119
+ id: post.id,
120
+ language: 'uk',
121
+ })
122
+ ```
123
+
124
+ Signature:
125
+
126
+ ```ts
127
+ getDocumentTranslation<TDoc>(args: {
128
+ collection: CollectionSlug
129
+ fieldNames?: MultilangFieldNames
130
+ id: number | string
131
+ language: string
132
+ }): Promise<TDoc | undefined>
133
+ ```
134
+
135
+ ## `findGlobalByLanguage`
136
+
137
+ Reads one language from a localized global and returns flat data for that language.
138
+
139
+ ```ts
140
+ const settings = await findGlobalByLanguage<SiteSettings>({
141
+ slug: 'site-settings',
142
+ language: 'uk',
143
+ })
144
+ ```
145
+
146
+ Signature:
147
+
148
+ ```ts
149
+ findGlobalByLanguage<TDoc>(args: {
150
+ depth?: number
151
+ language: string
152
+ slug: GlobalSlug
153
+ }): Promise<TDoc>
154
+ ```
155
+
156
+ ## `updateGlobalByLanguage`
157
+
158
+ Updates one language in a localized global.
159
+
160
+ ```ts
161
+ const settings = await updateGlobalByLanguage<SiteSettings>({
162
+ slug: 'site-settings',
163
+ language: 'uk',
164
+ data: {
165
+ title: 'Ukrainian title',
166
+ },
167
+ })
168
+ ```
169
+
170
+ Signature:
171
+
172
+ ```ts
173
+ updateGlobalByLanguage<TDoc>(args: {
174
+ data: Record<string, unknown>
175
+ depth?: number
176
+ language: string
177
+ slug: GlobalSlug
178
+ }): Promise<TDoc>
179
+ ```
180
+
181
+ The helper merges `data` into the selected language and preserves existing values in other languages.
182
+
183
+ ## Global Helpers With Payload
184
+
185
+ Use these variants when you already have a Payload instance or need request-aware access control.
186
+
187
+ ```ts
188
+ const settings = await findGlobalByLanguageWithPayload<SiteSettings>({
189
+ slug: 'site-settings',
190
+ language: 'uk',
191
+ overrideAccess: false,
192
+ payload: req.payload,
193
+ req,
194
+ })
195
+
196
+ await updateGlobalByLanguageWithPayload<SiteSettings>({
197
+ slug: 'site-settings',
198
+ language: 'uk',
199
+ data: {
200
+ title: 'Updated title',
201
+ },
202
+ overrideAccess: false,
203
+ payload: req.payload,
204
+ req,
205
+ })
206
+ ```
207
+
208
+ Signatures:
209
+
210
+ ```ts
211
+ findGlobalByLanguageWithPayload<TDoc>(args: {
212
+ depth?: number
213
+ language: string
214
+ overrideAccess?: boolean
215
+ payload: Payload
216
+ req?: PayloadRequest
217
+ slug: GlobalSlug
218
+ }): Promise<TDoc>
219
+
220
+ updateGlobalByLanguageWithPayload<TDoc>(args: {
221
+ data: Record<string, unknown>
222
+ depth?: number
223
+ language: string
224
+ overrideAccess?: boolean
225
+ payload: Payload
226
+ req?: PayloadRequest
227
+ slug: GlobalSlug
228
+ }): Promise<TDoc>
229
+ ```
230
+
231
+ `overrideAccess` defaults to `true`. Set it to `false` when operating on behalf of a user.
package/docs/usage.md ADDED
@@ -0,0 +1,269 @@
1
+ # Developer Usage
2
+
3
+ This guide covers the typical code you write in an app that uses `@roxxel/payload-multilang`.
4
+
5
+ ## Enable a Collection
6
+
7
+ Add the collection slug to the plugin config.
8
+
9
+ ```ts
10
+ payloadMultilang({
11
+ collections: {
12
+ posts: true,
13
+ },
14
+ languages: [
15
+ {
16
+ code: 'en',
17
+ isDefault: true,
18
+ name: 'English',
19
+ },
20
+ {
21
+ code: 'uk',
22
+ name: 'Ukrainian',
23
+ },
24
+ ],
25
+ })
26
+ ```
27
+
28
+ Editors can then choose the document language in the sidebar and create or connect translations from the collection list, document sidebar, or `Translations` edit tab.
29
+
30
+ ## Query Documents for One Language
31
+
32
+ Use `withLanguage()` with the Local API.
33
+
34
+ ```ts
35
+ import { getPayload } from 'payload'
36
+ import config from '@payload-config'
37
+ import { withLanguage } from '@roxxel/payload-multilang'
38
+
39
+ export const getPosts = async (language: string) => {
40
+ const payload = await getPayload({ config })
41
+
42
+ return payload.find({
43
+ collection: 'posts',
44
+ where: withLanguage({
45
+ language,
46
+ where: {
47
+ _status: {
48
+ equals: 'published',
49
+ },
50
+ },
51
+ }),
52
+ })
53
+ }
54
+ ```
55
+
56
+ Without an existing `where` query:
57
+
58
+ ```ts
59
+ const { docs } = await payload.find({
60
+ collection: 'posts',
61
+ where: withLanguage({
62
+ language: 'en',
63
+ }),
64
+ })
65
+ ```
66
+
67
+ With custom language field names:
68
+
69
+ ```ts
70
+ const where = withLanguage({
71
+ fieldName: 'postLanguage',
72
+ language: 'uk',
73
+ })
74
+ ```
75
+
76
+ ## Get Translations for a Document
77
+
78
+ Use `getDocumentTranslations()` when you have a document ID and need every linked translation.
79
+
80
+ ```ts
81
+ import { getDocumentTranslations } from '@roxxel/payload-multilang'
82
+
83
+ const state = await getDocumentTranslations({
84
+ collection: 'posts',
85
+ id: post.id,
86
+ })
87
+
88
+ const english = state.translations.en
89
+ const ukrainian = state.translations.uk
90
+ ```
91
+
92
+ The returned object is keyed by language code:
93
+
94
+ ```ts
95
+ type TranslationState<TDoc> = {
96
+ group?: string
97
+ language?: string
98
+ source?: TDoc
99
+ translations: Record<string, TDoc>
100
+ }
101
+ ```
102
+
103
+ Use `getDocumentTranslation()` when you only need one target language.
104
+
105
+ ```ts
106
+ import { getDocumentTranslation } from '@roxxel/payload-multilang'
107
+
108
+ const ukrainianPost = await getDocumentTranslation({
109
+ collection: 'posts',
110
+ id: post.id,
111
+ language: 'uk',
112
+ })
113
+ ```
114
+
115
+ ## Access Control in Server Code
116
+
117
+ The document translation helpers resolve Payload from `@payload-config` and use the default Local API access behavior. When you need user-scoped collection reads, query with Payload directly and pass `overrideAccess: false`.
118
+
119
+ ```ts
120
+ const { docs } = await req.payload.find({
121
+ collection: 'posts',
122
+ overrideAccess: false,
123
+ req,
124
+ where: withLanguage({
125
+ language: 'uk',
126
+ }),
127
+ })
128
+ ```
129
+
130
+ Use the global `WithPayload` helpers when you need request-aware access control for localized globals.
131
+
132
+ ## Enable a Global
133
+
134
+ Add the global slug to the plugin config.
135
+
136
+ ```ts
137
+ payloadMultilang({
138
+ globals: {
139
+ 'site-settings': true,
140
+ },
141
+ languages,
142
+ })
143
+ ```
144
+
145
+ Then read one language as flat data:
146
+
147
+ ```ts
148
+ import { findGlobalByLanguage } from '@roxxel/payload-multilang'
149
+
150
+ const settings = await findGlobalByLanguage({
151
+ slug: 'site-settings',
152
+ language: 'uk',
153
+ })
154
+
155
+ console.log(settings.title)
156
+ ```
157
+
158
+ Update one language without replacing other language values:
159
+
160
+ ```ts
161
+ import { updateGlobalByLanguage } from '@roxxel/payload-multilang'
162
+
163
+ await updateGlobalByLanguage({
164
+ slug: 'site-settings',
165
+ language: 'uk',
166
+ data: {
167
+ title: 'Ukrainian title',
168
+ },
169
+ })
170
+ ```
171
+
172
+ In request-scoped code:
173
+
174
+ ```ts
175
+ import { findGlobalByLanguageWithPayload } from '@roxxel/payload-multilang'
176
+
177
+ const settings = await findGlobalByLanguageWithPayload({
178
+ slug: 'site-settings',
179
+ language: 'uk',
180
+ overrideAccess: false,
181
+ payload: req.payload,
182
+ req,
183
+ })
184
+ ```
185
+
186
+ ## Keep Shared Fields in Sync
187
+
188
+ Use `synchronizedFields` for top-level fields that should be identical across translations, such as author, featured image, or canonical taxonomy.
189
+
190
+ ```ts
191
+ payloadMultilang({
192
+ collections: {
193
+ posts: {
194
+ synchronizedFields: ['featuredImage', 'author', 'category'],
195
+ },
196
+ },
197
+ languages,
198
+ })
199
+ ```
200
+
201
+ When an editor changes one of those fields on a saved translation, the plugin updates the same field on the other linked translations.
202
+
203
+ ## Create Empty Translations
204
+
205
+ By default, creating a translation duplicates compatible top-level source fields. To make new translations start empty:
206
+
207
+ ```ts
208
+ payloadMultilang({
209
+ collections: {
210
+ posts: {
211
+ duplicate: false,
212
+ },
213
+ },
214
+ languages,
215
+ })
216
+ ```
217
+
218
+ To duplicate most fields but exclude specific ones:
219
+
220
+ ```ts
221
+ payloadMultilang({
222
+ collections: {
223
+ posts: {
224
+ duplicateExcludeFields: ['slug', 'seoTitle', 'seoDescription'],
225
+ },
226
+ },
227
+ languages,
228
+ })
229
+ ```
230
+
231
+ ## Build Language Switchers
232
+
233
+ Use `getDocumentTranslations()` to render links to available translations.
234
+
235
+ ```tsx
236
+ import { getDocumentTranslations } from '@roxxel/payload-multilang'
237
+
238
+ export const LanguageSwitcher = async ({ collection, id }: { collection: 'posts'; id: string }) => {
239
+ type PostLink = {
240
+ id?: number | string
241
+ slug?: string
242
+ }
243
+
244
+ const state = await getDocumentTranslations<PostLink>({
245
+ collection,
246
+ id,
247
+ })
248
+
249
+ return (
250
+ <nav>
251
+ {Object.entries(state.translations).map(([language, doc]) => (
252
+ <a href={`/${language}/posts/${doc.slug || doc.id}`} key={language}>
253
+ {language.toUpperCase()}
254
+ </a>
255
+ ))}
256
+ </nav>
257
+ )
258
+ }
259
+ ```
260
+
261
+ Adjust the URL shape to match your frontend routing.
262
+
263
+ ## Generate Types
264
+
265
+ After enabling collections, globals, or changing field names, regenerate Payload types.
266
+
267
+ ```bash
268
+ pnpm run generate:types
269
+ ```
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "@roxxel/payload-multilang",
3
+ "version": "0.0.1",
4
+ "description": "Polylang-style document localization for Payload CMS",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./client": {
14
+ "import": "./dist/exports/client.js",
15
+ "types": "./dist/exports/client.d.ts",
16
+ "default": "./dist/exports/client.js"
17
+ },
18
+ "./rsc": {
19
+ "import": "./dist/exports/rsc.js",
20
+ "types": "./dist/exports/rsc.d.ts",
21
+ "default": "./dist/exports/rsc.js"
22
+ }
23
+ },
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "files": [
27
+ "dist",
28
+ "docs"
29
+ ],
30
+ "scripts": {
31
+ "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
32
+ "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
33
+ "build:types": "tsc --outDir dist --rootDir ./src",
34
+ "clean": "rimraf {dist,*.tsbuildinfo}",
35
+ "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
36
+ "dev": "next dev dev --turbo",
37
+ "dev:generate-importmap": "pnpm dev:payload generate:importmap",
38
+ "dev:generate-types": "pnpm dev:payload generate:types",
39
+ "dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
40
+ "generate:importmap": "pnpm dev:generate-importmap",
41
+ "generate:types": "pnpm dev:generate-types",
42
+ "lint": "eslint",
43
+ "lint:fix": "eslint ./src --fix",
44
+ "test": "pnpm test:int && pnpm test:e2e",
45
+ "test:e2e": "playwright test",
46
+ "test:int": "vitest"
47
+ },
48
+ "dependencies": {},
49
+ "devDependencies": {
50
+ "@eslint/eslintrc": "^3.2.0",
51
+ "@payloadcms/db-mongodb": "3.84.1",
52
+ "@payloadcms/db-postgres": "3.84.1",
53
+ "@payloadcms/db-sqlite": "3.84.1",
54
+ "@payloadcms/eslint-config": "3.28.0",
55
+ "@payloadcms/next": "3.84.1",
56
+ "@payloadcms/richtext-lexical": "3.84.1",
57
+ "@payloadcms/ui": "3.84.1",
58
+ "@playwright/test": "1.58.2",
59
+ "@swc-node/register": "1.10.9",
60
+ "@swc/cli": "0.6.0",
61
+ "@types/node": "22.19.9",
62
+ "@types/react": "19.2.14",
63
+ "@types/react-dom": "19.2.3",
64
+ "copyfiles": "2.4.1",
65
+ "cross-env": "^7.0.3",
66
+ "eslint": "^9.23.0",
67
+ "eslint-config-next": "16.2.6",
68
+ "graphql": "^16.8.1",
69
+ "mongodb-memory-server": "10.1.4",
70
+ "next": "16.2.6",
71
+ "open": "^10.1.0",
72
+ "payload": "3.84.1",
73
+ "prettier": "^3.4.2",
74
+ "qs-esm": "8.0.1",
75
+ "react": "19.2.6",
76
+ "react-dom": "19.2.6",
77
+ "rimraf": "3.0.2",
78
+ "sharp": "0.34.2",
79
+ "sort-package-json": "^2.10.0",
80
+ "typescript": "5.7.3",
81
+ "vite-tsconfig-paths": "6.0.5",
82
+ "vitest": "4.0.18"
83
+ },
84
+ "peerDependencies": {
85
+ "payload": "^3.84.1"
86
+ },
87
+ "engines": {
88
+ "node": "^18.20.2 || >=20.9.0",
89
+ "pnpm": "^9 || ^10 || ^11"
90
+ },
91
+ "publishConfig": {
92
+ "access": "public",
93
+ "registry": "https://registry.npmjs.org/"
94
+ }
95
+ }