VitNode
Internationalization (i18n)

Fields

How to use built-in input and WYSIWYG with i18n support in backend.

As an example, we will create a translation field for the title (input) and content (WYSIWYG) fields in the core_terms table.

Database

You need to create a new table in the database schema.

backend/plugins/{your_plugin}/admin/database/schema/terms.ts
import { pgTable, serial, timestamp, varchar } from 'drizzle-orm/pg-core';
 
export const core_terms = pgTable('core_terms', {
  id: serial('id').primaryKey(),
  code: varchar('code').notNull().unique(),
  created: timestamp('created').notNull().defaultNow(),
  updated: timestamp('updated').notNull().defaultNow(),
  href: varchar('href'),
});

You can find more information about the database schema in the documentation.

As you can see in the example above, we haven't added the translation fields yet. We will add them in service later using core_languages_words table.

Show Translation

DTO Object

For the translation fields to show data, you need to use StringLanguage[] type in the DTO for the object.

shared/plugins/{your_plugin}/legal.dto.ts
import { ApiProperty } from '@nestjs/swagger';
 
import { StringLanguage } from 'vitnode-shared/string-language.dto'; 
import { PaginationObj } from 'vitnode-shared/utils/pagination.dto';
 
export class Legal {
  @ApiProperty({ type: [StringLanguage] }) 
  content: StringLanguage[]; 
 
  @ApiProperty({ type: [StringLanguage] }) 
  title: StringLanguage[]; 
}
 
export class LegalsObj extends PaginationObj {
  @ApiProperty({ type: [Legal] })
  edges: Legal[];
}

DTO for multipart/form-data

If you are using multipart/form-data all fields will be string type. You need to transform them to StringLanguage[] type in the service.

shared/plugins/{your_plugin}/legal.dto.ts
export class Legal {
  @ApiProperty({ type: [StringLanguage] })

  @Transform(({ value }: { value: string }) => {

    const current = JSON.parse(value);

 

    return Array.isArray(current) ? current : [current];

  })
  title: StringLanguage[];
}

Service

core_languages_words table doesn't have relation so you need to use StringLanguageHelper service with show() to get translations.

In return service we will filter translation by item_id and variable to get the correct translation for the title and content fields.

backend/plugins/{your_plugin}/terms/show/show.service.ts
import { core_legal } from '@/database/schema/legal';
import { StringLanguageHelper } from 'vitnode-backend/helpers/string_language/helpers.service'; 
import { DatabaseService } from '@/database/database.service';
import { Injectable } from '@nestjs/common';
import { LegalsObj, LegalsQuery } from 'shared/legal.dto';
import { SortDirectionEnum } from 'vitnode-shared/utils/pagination.enum';
 
@Injectable()
export class ShowLegalService {
  constructor(
    private readonly databaseService: DatabaseService,
    private readonly stringLanguageHelper: StringLanguageHelper, 
  ) {}
 
  async show({ cursor, first, last }: LegalsQuery): Promise<LegalsObj> {
    const pagination = await this.databaseService.paginationCursor({
      cursor,
      database: core_legal,
      first,
      last,
      defaultSortBy: {
        direction: SortDirectionEnum.desc,
        column: 'updated_at',
      },
      query: async args =>
        await this.databaseService.db.query.core_legal.findMany({
          ...args,
        }),
    });
 
    const ids = pagination.edges.map(edge => edge.id); 

    const i18n = await this.stringLanguageHelper.get({

      item_ids: ids,

      database: core_legal,

      plugin_code: 'core',

      variables: ['title', 'content'],

    });
 
    const edges = pagination.edges.map(edge => {
      const currentI18n = i18n.filter(item => item.item_id === edge.id);
 
      return {
        ...edge,

        title: currentI18n

          .filter(value => value.variable === 'title')

          .map(value => ({

            value: value.value,

            language_code: value.language_code,

          })),

        content: currentI18n

          .filter(value => value.variable === 'content')

          .map(value => ({

            value: value.value,

            language_code: value.language_code,

          })),
      };
    });
 
    return { ...pagination, edges };
  }
}

API Reference

PropTypeDefault
item_ids
number[]
-
database
DatabaseSchema
-
plugin_code
string
-
variables
string[]
-

Create / Edit Translation

DTO Input

For the translation fields to work in the input, you need to use StringLanguage type in the DTO for the input and class-validator decorators.

shared/plugins/{your_plugin}/admin/legal.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { ArrayMinSize, IsArray } from 'class-validator';
import { StringLanguage } from 'vitnode-shared/string-language.dto';
 
export class CreateLegalSettingsAdminBody {
  @ApiProperty({ type: [StringLanguage] })
  @ArrayMinSize(1)
  @IsArray()
  content: StringLanguage[];
 
  @ApiProperty({ type: [StringLanguage] })
  @ArrayMinSize(1)
  @IsArray()
  title: StringLanguage[];
}

To required translation fields you can use @ArrayMinSize(1) decorator.

Service

To create or edit translation fields you need to use StringLanguageHelper service with parse() method.

As an example, we will create a translation field for the title and content fields in the core_terms table.

backend/plugins/{your_plugin}/terms/create/create.service.ts
import { core_legal } from '@/database/schema/legal';
import { removeSpecialCharacters } from 'vitnode-backend/functions';
import { StringLanguageHelper } from 'vitnode-backend/helpers/string_language/helpers.service'; 
import { DatabaseService } from '@/database/database.service';
import { ConflictException, Injectable } from '@nestjs/common';
import { CreateLegalSettingsAdminBody } from 'shared/admin/settings/legal.dto';
import { Legal } from 'shared/legal.dto';
 
@Injectable()
export class CreateLegalSettingsAdminService {
  constructor(
    private readonly databaseService: DatabaseService,
    private readonly stringLanguageHelper: StringLanguageHelper, 
  ) {}
 
  async create({
    title,
    content,
    href,
    code,
  }: CreateLegalSettingsAdminBody): Promise<Legal> {
    const termExist = await this.databaseService.db.query.core_legal.findFirst({
      where: (table, { eq }) => eq(table.code, code),
    });
 
    if (termExist) {
      throw new ConflictException('LEGAL_ALREADY_EXISTS');
    }
 
    const [term] = await this.databaseService.db
      .insert(core_legal)
      .values({ href, code: removeSpecialCharacters(code) })
      .returning();
 

    const titleTerm = await this.stringLanguageHelper.parse({

      item_id: term.id,

      plugin_code: 'core',

      database: core_legal,

      data: title,

      variable: 'title',

    });
 

    const contentTerm = await this.stringLanguageHelper.parse({

      item_id: term.id,

      plugin_code: 'core',

      database: core_legal,

      data: content,

      variable: 'content',

    });
 
    return {
      ...term,
      title: titleTerm,
      content: contentTerm,
    };
  }
}

This method will create a translation inside core_languages_words table and processing content to send notifications, attachments, etc.

API Reference

PropTypeDefault
item_id
number
-
plugin_code
string
-
database
DatabaseSchema
-
data
StringLanguageInput[]
-
variable
string
-

Delete Service

As we mention before, when you create or edit translation fields then our parser() method will processing content. To delete translations and other things related to it you need to use delete() method.

backend/plugins/{your_plugin}/terms/delete/delete.service.ts
import { core_legal } from '@/database/schema/legal';
import { StringLanguageHelper } from 'vitnode-backend/helpers/string_language/helpers.service'; 
import { DatabaseService } from '@/database/database.service';
import { Injectable, NotFoundException } from '@nestjs/common';
import { eq } from 'drizzle-orm';
 
@Injectable()
export class DeleteLegalSettingsAdminService {
  constructor(
    private readonly databaseService: DatabaseService,
    private readonly stringLanguageHelper: StringLanguageHelper, 
  ) {}
 
  async delete(code: string): Promise<void> {
    const term = await this.databaseService.db.query.core_legal.findFirst({
      where: (table, { eq }) => eq(table.code, code),
      columns: {
        id: true,
      },
    });
 
    if (!term) {
      throw new NotFoundException();
    }
 

    await this.stringLanguageHelper.delete({

      database: core_legal,

      item_id: term.id,

      plugin_code: 'core',

    });
 
    await this.databaseService.db
      .delete(core_legal)
      .where(eq(core_legal.id, term.id));
  }
}

Always delete translation when delete primary record!

This is important to delete translation. Otherwise, translations will be still in the database.

API Reference

PropTypeDefault
database
DatabaseSchema
-
item_id
number
-
plugin_code
string
-

On this page