Hi, I'm trying to create a way to implement translations for fields in Payload. What can I do to achieve that?
Example:
Consider as an example the following Json representing a Player.
"Player": {
"name" : [
{ "id" : 1, "language" : "english", "key" : "name", "value" : "John" }, // Translation relationship
{ "id" : 2, "language" : "portuguese", "key" : "name", "value" : "João" }, // Translation relationship
],
"age" : 18,
"description" : [
{ "id" : 3, "language" : "english", "key" : "description", "value" : "Hello World!" }, // Translation relationship
{ "id" : 4, "language" : "portuguese", "key" : "description", "value" : "Olá Mundo!" }, // Translation relationship
]
}How could I achieve a representation of this with as many languages and translations I wish?
I was looking into defining the Collection with the fields that can be translated as an array of relationships. But I'm not sure if that's even possible.
const languageFields: Field[] = [
{ name: 'name', type: 'text' },
{ name: 'isDefault', type: 'checkbox' }
]
const Languages: CollectionConfig = {
slug: 'languages',
auth: true,
admin: { useAsTitle: 'name' },
fields: languageFields,
}
const translationFields: Field[] = [
{ name: 'language', type: 'relationship', relationTo: 'languages', required: true },
{ name: 'key' , type: 'text' , },
{ name: 'value' , type: 'text' , },
]
const Translations: CollectionConfig = {
slug: 'translations',
auth: true,
admin: { useAsTitle: 'name' },
fields: translationFields,
};
const playerFields: Field[] = [
{ name: 'name', type: 'array', fields: [{ name: 'translation', type: 'relationship', relationTo: 'translations' }] },
{ name: 'age' , type: 'number' },
{ name: 'description', type: 'array', fields: [{ name: 'translation', type: 'relationship', relationTo: 'translations' }] }
]
const Players: CollectionConfig = {
slug: 'players',
auth: true,
admin: { useAsTitle: 'name' },
fields: playerFields,
}
export { Languages, Translations, Players }This is my attempt. Not sure if this makes even sense. Could someone help me with this?
Hey
@324517054667423744,
I would highly encourage you to take a look at the docs, as it looks like what you're trying to do is already covered by localization - in which case all you would have to do is configure it and author your content. See here:
https://payloadcms.com/docs/configuration/localizationThat doesn't serve me in this case. It will be useful for some other stuff later on, but rn I really need to make it in the DB level. Can't make it using Localization. I was looking into the
Joinfield. Not sure if it is the right place to be looking at
Yeah, looks like
relationshipand
joinfields will be your friends
Could you give me an example of how
joinfields would work here? I'm reading the documentation but the example given looks a bit difficult to understand.
Certainly! Say you have a
playerscollection which holds all of the players of your game, and you have a
languagescollection which holds spoken languages that players may use. Now, suppose each player has a relationship to languages via a field named
spokenLanguage:
collection data...
{
name: 'spokenLanguage',
type: 'relationship',
relationTo: 'languages'
}In your
languagescollection, it may be useful to see which players speak a particular language, and you would achieve this directional flow of information via a
joinfield, like so:
... rest of languages collection field
{
name: 'playersWhoSpeakMe',
type: 'join',
collection: 'players',
on: 'spokenLanguage',
}Does this make sense?
Ok, so would this still work if I have it on 2 different fields?
p.e. in the example both the Player's fields 'name' and 'description' are translatable. Would they be both of type
join? and if so what would be the
oni would refer to?
Join represents a bidirectional relationship, I'm not sure where you would put joins for those, like from what collection?
In a relationship i could just say that certain field references an instance of a collection. Right?
I just want an equivalent to a One-to-Many relationship. If you will. But independent from the collection. Rn I'm using the Player collection as an example but later on I want translation of other collection's fields.
For on-to-many, you would juse use a relationship field with a
hasManyoption set to true
Uhm ok, that is probably what I want.
The
relationTooption is supposed to take the
slugof the collection i want to reference, right?
Yep! Or an array of slugs if it's hasMany
In my case I'm getting a
Collection slug already in useerror.
But i'm looking around and I can't find another place where I'm using it.
You mean in your relatinoship field?
Is it hasMany or a single?
In this case. (sorry for the print)
Its throwing an error that
"languages"is already a slug in use.
Huh
This is strange, what if you change the name of that field?
from Language to something else
Changed to 'lingua' and it gave the same error.
Do you need it to be an auth collection? Can you try removing auth: true
Otherwise I'm not too sure what's happening here
Just to be sure, the
authoption makes it visible only if its authenticated. Right?
No, auth option makes it so that payload recognizes it as a collection that can have users
As in it adds email and password fields to it automatically
AH. then no it ain't needed
You are looking for
accessTo get what you described above
I removed it. Nothing changed
oh wait a min. I'm dumb
nvm
I was giving it duplicated
Yep, that'll do it
Huh. Thats a new one. I'm getting an error telling me that I'm
not allowed to perform this actionwhile trying to access
/adminCheck access controls and CRSF in config
On BuildConfig or the Initialization?
Buildconfig, check access controls on your collections
I haven't specified anything. Do I need to?
I think it's this giving you trouble:
https://github.com/payloadcms/payload/blob/main/templates/website/src/payload.config.ts#L67Can you double check you have that
cors?
I don't have that on my buildConfig. But I did define
app.use(cors( { origin: '*' } ));before initializing the Payload
Do it in buildConfig
In general, the template I linked you to is a great resource
The same problem persists
Are you on v3 or v2?
v3 I believe
Mind sharing your buildConfig?
Sure
import { ENV } from './env';
import WebPackConfig from "@/webpack.config"
import path from 'path'
import { payloadCloud } from '@payloadcms/plugin-cloud'
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { webpackBundler } from '@payloadcms/bundler-webpack'
import { slateEditor } from '@payloadcms/richtext-slate'
import { buildConfig } from 'payload/config'
import { Translations, Languages, Users, Teams, Persons } from './collections'
console.log("Before Config")
export default buildConfig({
editor: slateEditor({}),
collections: [ Translations, Users, Teams, Persons, Languages ],
typescript: { outputFile: path.resolve(__dirname, 'payload-types.ts') },
graphQL: { schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql') },
plugins: [payloadCloud()],
db: mongooseAdapter({ url: ENV.PAYLOAD_PUBLIC_DATABASE_URI, schemaOptions: { strict: false }, }),
admin: {
bundler: webpackBundler(),
},
cors: '*'
})
console.log("After Config")I decided to check the console of the page while loading
I found this:
Can you move your cors config to how it is in the template, create an env var for NEXT_PUBLIC_SERVER_URL as it's used quite a bit
The issue you have is symptomatic of misconfigured cors
I'm currently only working on localhost. And shouldn't the
'*'do the trick?
It's not clear, I think it depends on credential headers and some other stuff
Wildcards on localhost
What do you mean?
I agree with you, yes technically it should work, but it's not clear to me if it will here since the issue you are describing is typical of misconfigured cors. Might be that the config is expecting a real url
Shouldn't this come from the access control of the collections?
Did you check access controls? You've configued custom access for your collections?
If you've not configured anything custom for your collections, access controls will default to true
But this was once running without anything related to cors
Did this happen after you removed auth from login?
Do you have a
userscollection?
Yes I do
Is there a user in there that's already created?
Its just as simple as this:
import { CollectionConfig } from 'payload/types'
const Users: CollectionConfig = {
slug: 'users',
auth: true,
admin: {
useAsTitle: 'email',
},
fields: [
// Email added by default
// Add more fields as needed
],
}
export { Users };Yes
Should I reset the MongoDB?
Nice, try logging out, and then logging in with that user
I think maybe something with removing an auth collection went goofy
I
alsothink the cors wildcard should work but here we are
I can't. Normally when I load /admin it shows me the login page or the register page if no user is available
Clear cookies, or open in incognito, restart server and give it a try
Ok, let me restart stuff then
Nope, I restarted everything. Even the DB
Still the same problem
Yeah I checked in the meantime, cors accepts string[] | "*" | CORSConfig
As for Access Controls, by default all it will do is check that a user is logged in essentially
Not too sure what's giving you the trouble
Mind if I share with you my Collections files?
Sure
Person.tsimport { CollectionConfig, CollectionAfterChangeHook, Field } from 'payload/types';
import { cache } from '../middlewares';
const PERSON_MEMCLEAN = [
'player_stats',
'team_roster',
'double_subs',
'team_tactic_substitutes',
'match_tactics',
'match_scorers',
'player_touchmap',
'player_heatmap',
'player_compare_match',
'player_seasonstats',
'player_compare_season',
'top5',
'team_tactic_l3',
];
const personFields: Field[] = [
{ name: 'matchName' , type: 'relationship', relationTo: 'translations', hasMany: true },
{ name: 'shortName' , type: 'relationship', relationTo: 'translations', hasMany: true },
{ name: 'firstName' , type: 'text' },
{ name: 'lastName' , type: 'text' },
{ name: 'image' , type: 'text' },
{ name: 'video' , type: 'text' },
{ name: 'vbn_folder', type: 'text' },
{ name: 'team' , type: 'relationship', relationTo: 'teams', admin: { readOnly: true } },
]
// Define the afterChange hook
const afterChangeHook: CollectionAfterChangeHook = async ({ doc }) => {
cache.deletetargetkeys(PERSON_MEMCLEAN);
};
const Persons: CollectionConfig = {
slug: 'persons',
admin: {
useAsTitle: 'matchName',
defaultColumns: ['firstName', 'lastName', 'team'],
},
fields: personFields,
hooks: { afterChange: [afterChangeHook] },
};
export { Persons };Team.tsimport { CollectionConfig, CollectionAfterChangeHook, Field } from 'payload/types';
import { cache } from '../middlewares';
const teamFields: Field[] = [
{ name: 'name' , type: 'relationship', relationTo: 'translations', hasMany: true },
{ name: 'initial' , type: 'relationship', relationTo: 'translations', hasMany: true },
{ name: 'officialName', type: 'text' },
{ name: 'imagesfolder', type: 'text' },
{ name: 'color' , type: 'text' },
{ name: 'textcolor' , type: 'text' },
{ name: 'numbercolor' , type: 'text' },
{ name: 'badge' , type: 'text' },
];
const afterChangeHook: CollectionAfterChangeHook = async ({ doc }) => {
cache.deletekeys(['&target=']);
};
const Teams: CollectionConfig = {
slug: 'teams',
admin: { useAsTitle: 'officialName', defaultColumns: ['officialName'] },
fields: teamFields,
hooks: { afterChange: [afterChangeHook] },
};
export { Teams };Translation.tsimport { CollectionConfig, CollectionAfterChangeHook, Field, CollectionAfterLoginHook } from 'payload/types'
import { cache } from '../middlewares';
// Languages Collection
const languageFields: Field[] = [
{ name: 'name', type: 'text' },
{ name: 'isDefault', type: 'checkbox' }
]
const Languages: CollectionConfig = {
slug: 'languages',
admin: { useAsTitle: 'name' },
fields: languageFields,
}
const translationFields: Field[] = [
{ name: 'key' , type: 'text' },
{ name: 'language', type: 'relationship', relationTo: 'languages' },
{ name: 'value' , type: 'text' },
]
const afterChangeHook: CollectionAfterChangeHook = async ({ doc }) => { cache.deletekeys(['&target=']); };
const Translations: CollectionConfig = {
slug: 'translations',
admin: { useAsTitle: 'name' },
fields: translationFields,
hooks: { afterChange: [afterChangeHook] },
};
export { Languages, Translations };User.tsimport { CollectionConfig } from 'payload/types'
const Users: CollectionConfig = {
slug: 'users',
auth: true,
admin: {
useAsTitle: 'email',
},
fields: [
// Email added by default
// Add more fields as needed
],
}
export { Users };Do you think there's something I'm not supposed to do?
(Btw thank you for helping me, im sure the topic of this thread is long gone)
What's cache from middlewares?
I'm not seeing anything here that stands out that would cause your issue
And it's my pleasure
Its just a place where I have a few functions to treat the URL so I can store them
import mcache from 'memory-cache';
const verify = (duration) => {
return (req, res, next) => {
const key = '__express__' + req.originalUrl || req.url
const cachedBody = mcache.get(key)
if (cachedBody) {
res.send(cachedBody)
} else {
res.sendResponse = res.send
res.send = (body) => {
mcache.put(key, body, duration * 1000);
res.sendResponse(body)
}
next()
}
}
};
const deletekeys = (keys) => {
keys.forEach(key => {
let cachekeys = mcache.keys().filter(element => element.includes(key))
cachekeys.forEach(element => {
mcache.del(element)
});
});
};
const deletetargetkeys = (targets) => {
targets.forEach(target => {
let cachekeys = mcache.keys().filter(element => element.includes(`&target=${target}`))
cachekeys.forEach(element => {
mcache.del(element)
});
});
};
const deletetargetkeysandmatch = (target, keys) => {
keys.forEach(key => {
let cachekeys = mcache.keys().filter(element => element.includes(`&target=${target}`) && element.includes(key))
cachekeys.forEach(element => {
mcache.del(element)
});
});
};
export {
verify,
deletekeys,
deletetargetkeys,
deletetargetkeysandmatch
};Sorry for the late response btw
Hi,
@654031862146007055just to update you.
Ok, so I've been trying some stuff. Found this thread for someone using v2 that were also getting a forbidden request. The solution was adding an explicit access control to the collection. I did that to each collection.
const collection: CollectionConfig = {
...
access: {
read : () => true, // allows public GET
update: () => true, // allows public PATCH
create: () => true, // allows public POST
delete: () => true, // allows public DELETE
}
}And that did made the forbidden request disappear. Although now those transformed into 404 requests.
ERROR (payload): NotFound: The requested resource was not found.
at findByID (D:\backoffice-keystone-opta\back-office-opta\node_modules\payload\src\collections\operations\findByID.ts:99:15)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at findByIDHandler (D:\backoffice-keystone-opta\back-office-opta\node_modules\payload\src\collections\requestHandlers\findByID.ts:26:17)This is weird
You haven't changed the default admin route have you?
I'm trying to think of what could be causing 404's
/admin ?
That issue with the access I haven't seen since v2 which makes me think you're on 2.x actually
Yeah /admin
How can I make it to be sure?
Can you actually run npm list payload or pnpm why payload
You should see direct version nums
Fu- apparently I am in fact using 2.0
wtf
Oh man, yeah
This was eerily reminiscent of an issue I saw in v2
I think it can be solved, although I don't remember what the root cause was
Can I upgrade it to v3?
But v3 was released just yesterday
Yeah 1 sec
I have a doc for you
Cuz I've been following the v3 docs xD
Oh you may have been running into all sorts of unexpected issues if you've been following v3 docs
v3 just went stable yesterday
It may be worth just starting a new website template using npx or pnpm dlx create-payload-app
And then just copy + pasting your configs
Instead of trying to migrate outright
Might save you some time
Shoot, should've asked sooner - sorry I didn't catch that
wait... so when I was following the docs instalation process I did the
npx create-payload-appwhich asked me if i remember correctly which version i wanted to proceed with. I can with like 95% certainty say that I picked v3
I started 1 week ago on PayloadCMS
Ahhh, but cpa was still in beta then, and cpa may have not been configured to scaffold a v3 app from the base command
You would've had to run create-payload-app@beta
I cant check it with certainty
You could check package.json
Or lockfile
If it's v3 beta -> it'll be tagged with
@beta.123or some number
v3 is on
3.0.1v2 is
2.x.xBut where?
I dont see that anywhere
In package.json, look for payload package
{
"name": "BackOffice-Opta",
"description": "A blank template to get started with Payload",
"version": "1.0.0",
"main": "dist/server.js",
"license": "MIT",
"scripts": {
"dev": "env-cmd nodemon src/server.ts ",
"build:payload": "env-cmd payload build",
"build:server": "tsc",
"build": "yarn copyfiles && yarn build:payload && yarn build:server",
"serve": "env-cmd node dist/server.js",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
"generate:types": "env-cmd payload generate:types",
"generate:graphQLSchema": "env-cmd payload generate:graphQLSchema",
"payload": "env-cmd payload"
},
"dependencies": {
"@payloadcms/bundler-webpack": "^1.0.0",
"@payloadcms/db-mongodb": "^1.0.0",
"@payloadcms/plugin-cloud": "^3.0.0",
"@payloadcms/richtext-slate": "^1.0.0",
"assert": "^2.1.0",
"axios": "^1.7.7",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"dotenv": "^16.4.5",
"env-cmd": "^10.1.0",
"express": "^4.19.2",
"google-spreadsheet": "^3.3.0",
"luxon": "^3.5.0",
"memory-cache": "^0.2.0",
"mongoose": "^8.8.0",
"node-cron": "^3.0.3",
"os-browserify": "^0.3.0",
"payload": "latest",
"promise.allsettled": "^1.0.7",
"querystring-es3": "^0.2.1",
"stream-browserify": "^3.0.0",
"url": "^0.11.4",
"util": "^0.12.5",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.9.0",
"copyfiles": "^2.4.1",
"nodemon": "^2.0.20",
"ts-node": "^9.1.1",
"typescript": "^4.9.5",
"webpack": "^5.96.1",
"webpack-cli": "^5.1.4"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}Judging from the plugins, the fact that you don't have react/react-dom and no next package in deps... you're on v2
;-;
Damn
Also v3 doesn't use Express anymore
It's gonna be a nice upgrade though
I'm still using express tho, I'm using for a separated API running in the same stuff
Ahh, that's fine
Might need to have some seperation from the Payload stuff
But Payload is headless so it doesn't care too much
Its completely separated
Oh you're all good
Payload v2 used Express internally
That dep was dropped in v3
Oh so what does it use rn?
NextJS
It's NextJS native
uh
No wonder i was having so much trouble with stuff that was supposed to work
My mind is being blown right now, you have the patience of a saint
I can only imagine the trouble you were running into
Ahahah, no problem. People tell me that on a daily basis
Let me know if you run into anymore issues going forward!
Ok, ill probably be opening a different thread for the problems going forward tho
This one is getting really extensive the topic is no longer relevant
Star
Discord
online
Get dedicated engineering support directly from the Payload team.