🛠️ VitNode is still in development! You can try it out, but it is not recommended to use it now in production.
Backend
Internationalization (i18n)

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 with varchar type,
  • value with varchar type (You can set length 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.