Internationalization (i18n)
Create a new multilanguage input in backend maight be a bit tricky, because VitNode dosn't provide any functions for that. But don't worry, it's not that hard. You have to follow few rules and you will be fine.
Database schema
For exaple we use core_nav
table.
Create main table
Create a schema core_nav
in database
folder.
export const core_nav = pgTable("core_nav", {
id: serial("id").primaryKey(),
href: varchar("href", { length: 255 }).notNull(),
external: boolean("external").notNull().default(false),
position: integer("position").notNull().default(0)
});
Translation table
Create a schema core_nav_name
in the same file for translation table.
The translation table must have:
language_code
withvarchar
type,value
withvarchar
type (You can setlength
option),- any fields for relation with main table (in our case
nav_id
)
export const core_nav_name = pgTable("core_nav_name", {
id: serial("id").primaryKey(),
nav_id: serial("nav_id")
.notNull()
.references(() => core_nav.id, {
onDelete: "cascade"
}),
language_code: varchar("language_code")
.notNull()
.references(() => core_languages.code, {
onDelete: "cascade"
}),
value: varchar("value", { length: 50 }).notNull()
});
Remember to set onDelete: 'cascade'
action into references. We want to delete translation when we delete main table.
Relation
Add relation to main table.
export const core_nav_relations = relations(core_nav, ({ many }) => ({
name: many(core_nav_name)
}));
You can read more about relations using Drizzle here (opens in a new tab).
Input field mutation
For your mutation we're created for you a new type TextLanguageInput
.
Here is a code for example:
import { ArgsType, Field } from "@nestjs/graphql";
import { IsArray } from "class-validator";
import { Transform } from "class-transformer";
import {
IsTextLanguageInput,
TextLanguageInput,
TransformTextLanguageInput
} from "@/types/database/text-language.type";
@ArgsType()
export class CreateAdminNavArgs {
@IsArray()
@IsTextLanguageInput()
@Transform(TransformTextLanguageInput)
@Field(() => [TextLanguageInput])
name: TextLanguageInput[];
}
If you want to change this field to required
, you have to add some decorators form class-validator
.
import { ArgsType, Field } from "@nestjs/graphql";
import { ArrayMinSize, IsArray, ValidateNested } from "class-validator";
import { Transform } from "class-transformer";
import {
IsTextLanguageInput,
TextLanguageInput,
TransformTextLanguageInput
} from "@/types/database/text-language.type";
@ArgsType()
export class CreateAdminNavArgs {
@IsArray()
@ValidateNested({ each: true })
@ArrayMinSize(1)
@IsTextLanguageInput()
@Transform(TransformTextLanguageInput)
@Field(() => [TextLanguageInput])
name: TextLanguageInput[];
}
Object field mutation
For your mutation we're created for you a new type TextLanguage
.
Here is a code for example:
import { Field, ObjectType } from '@nestjs/graphql';
import { TextLanguage } from '@/types/database/text-language.type';
@ObjectType()
class ShowCoreNavItem {
@Field(() => [TextLanguage])
name: TextLanguage[];
...
}
Create Mutation
Now let's create a mutation for adding new record to database with translations.
Create record
Create a record in main table and get the id.
const nav = await this.databaseService.db
.insert(core_nav)
.values({
href,
external
})
.returning();
const id = nav[0].id;
Create translations
Create translations for each language.
const namesNav = await this.databaseService.db
.insert(core_nav_name)
.values(
name.map(n => ({
nav_id: id,
language_code: n.language_code,
value: n.value
}))
)
.returning();
But if you have empty array of translations, you have to handle it. You can't create empty translation.
We want description
to be optional, so we have create an empty array if it's not passed.
const descriptionNav =
description.length > 0
? await this.databaseService.db
.insert(core_nav_description)
.values(
description.map(n => ({
nav_id: id,
language_code: n.language_code,
value: n.value
}))
)
.returning()
: [];
Update Mutation
In this case update data is a bit different.
You have to check if the translation:
- exists, update it,
- doesn't exist, create it,
- exists, but the value is empty or not passed, delete it.
It may sound complicated, but it's not. We will show you how to do it.
Remember to always check if redord exists before you update it. If it doesn't exist, you have to throw an error.
Get translation
const names = await this.databaseService.db.query.core_nav_name.findMany({
where: (table, { eq }) => eq(table.nav_id, id)
});
Process data
const update = await Promise.all(
name.map(async item => {
const itemExist = names.find(el => el.language_code === item.language_code);
if (itemExist) {
// If value is empty, do nothing
if (!itemExist.value.trim()) return;
const update = await this.databaseService.db
.update(core_nav_name)
.set({ ...item, nav_id: id })
.where(eq(core_nav_name.id, itemExist.id))
.returning();
return update[0];
}
const insert = await this.databaseService.db
.insert(core_nav_name)
.values({ ...item, nav_id: id })
.returning();
return insert[0];
})
);
Delete remaining translations
Promise.all(
names.map(async item => {
const exist = update.find(name => name.id === item.id);
if (exist) return;
await this.databaseService.db
.delete(core_nav_name)
.where(eq(core_nav_name.id, item.id));
})
);
Delete Mutation
If you add onDelete: Cascade
action to relation, you don't have to do anything. When you delete main record, all translations will be deleted.
Frontend
Backend is ready? Great! Now let's move to frontend.