Skip to content

Translations

This section will cover how translations are handled within the Sodacore framework, allowing you to provide localized content for your application.

Setting up translations.

We shall provide two translations for Spanish and Portuguese, but you can add as many as you like, the files are named after the locale code, i.e. for Spanish it would be es-ES.json and for Portuguese it would be pt-PT.json, you can also use en-GB.json for English.

Create a new folder in the root of your project called translations, and inside that folder create two files, es-ES.json and pt-PT.json.

Then copy the following content into each file:

json
// ./translations/es-ES.json
{
	"Tasks Demo": "Demostración de tareas",
	"Enter your username": "Ingrese su nombre de usuario",
	"Username": "Nombre de usuario",
	"Start Chatting": "Empezar a chatear",
	"Type a message": "Escribe un mensaje",
	"Delete Task": "Eliminar tarea",
	"Title": "Título",
	"Description": "Descripción",
	"Task description": "Descripción de la tarea",
	"Create Task": "Crear tarea",
	"Connected": "Conectado",
	"Add Task": "Agregar tarea",
	"Tasks List": "Lista de tareas",
	"Cancel": "Cancelar",
	"Task title": "Título de la tarea",
	"Mark as Incomplete": "Marcar como incompleto",
	"Mark as Complete": "Marcar como completo"
}

And for Portuguese:

json
// ./translations/pt-PT.json
{
	"Tasks Demo": "Demonstração de tarefas",
	"Enter your username": "Introduza o seu nome de utilizador",
	"Username": "Nome de utilizador",
	"Start Chatting": "Comece a conversar",
	"Type a message": "Introduza uma mensagem",
	"Delete Task": "Apagar tarefa",
	"Title": "Título",
	"Description": "Descrição",
	"Task description": "Descrição da tarefa",
	"Create Task": "Criar tarefa",
	"Connected": "Conectado",
	"Add Task": "Adicionar tarefa",
	"Tasks List": "Lista de tarefas",
	"Cancel": "Cancelar",
	"Task title": "Título da tarefa",
	"Mark as Incomplete": "Marcar como incompleto",
	"Mark as Complete": "Marcar como concluído"
}

This will cover the basic translations for our application, as noted above, you can add as many as you need.

The way we have designed the translations is that they are loaded from the translations folder at runtime, and are cached within the application memory, so of course if you have 100s of translations with large files, you may want to consider a different approach, such as loading them from a database or an external service, see the I18n Plugin for more information on how to do this.

Recap our configuration.

If we look back at the src/main.ts file, we can see that we have already configured the I18n plugin to use the translations folder:

typescript
// Install the I18n plugin.
app.use(new I18nPlugin({
	defaultLocale: 'en-GB',
}));

By default, the I18n plugin will look for the translations in a root folder called translations which is why we have not defined it, but you can override the folder by passing the translationsPath option with an absolute path to the translations folder, we suggest absolute because depending on where the code is can affect relative paths, etc. Keep it simple.

We have also defined the defaultLocale to be en-GB, which is the default locale for our application.

Translations can be setup a variety of ways, either using keys/codenames for each property, or by using normal text, Sodacore supports both, but to keep it simple, what we do is the whole frontend is written in English, wrapped in _t() tags, these are "translation tags" that will be replaced with the appropriate translation based on the user's locale when using the autoTranslate method, which is provided by the I18n provider.

So for our example, rather than using code names, we use plain English text, and then we take each "string" of text and create the translations based on that, using the English text as the key, when we mix this with the defaultLocale option, the plugin will add the default locale to the translations mix, as the "default", so that when the user comes to our platform, we can get their "supported" locale from the request headers, using the Accept-Language header, and then we can use that to determine which translation to use, example:

typescript
//...
const acceptLanguage = request.headers.get('Accept-Language') || '';
const userLocale = this.translator.getAvailableTranslation(acceptLanguage) || 'en-GB';
//...

The translator is the I18nProvider and has a function called getAvailableTranslation which will return the best matching translation based on the user's locale, or the default locale if none is found.

Then when we pass this information to the autoTranslate method, it will return the translated text based on the user's locale, now by default, if the locale is the default locale, it will return the original text, without the _t() tags, so you can use the same text in your frontend code without having to worry about translations.

Using translations in your application.

Our main index.html file will use the _t() tags to wrap the text we want to translate, so let's amend the ./src/controller/app.ts file to use the translations we have defined.

typescript
import { Controller, Get, Params } from '@sodacore/http'; 
import { Controller, Get, Params, Query, Request } from '@sodacore/http'; 
import { resolve } from 'node:path';
import { file } from 'bun';
import { Inject } from '@sodacore/di'; 
import { I18nProvider } from '@sodacore/i18n'; 

@Controller('/')
export default class TodoController {
	private readonly basePath = resolve(process.cwd(), './public');

	@Get('/')
	public async index() { 
	public async index( 
		@Request() request: Request, 
		@Query('locale') locale?: string, 
	) { 

		// Get the index file.
		const indexPath = resolve(this.basePath, 'index.html');
		const indexFile = file(indexPath); 
		const fileContent = await file(indexPath).text(); 

		// Check available locale.
		const acceptLanguage: string = request.headers.get('accept-language') ?? 'en-GB'; 
		const availableLanguageCode = locale || this.translator.getAvailableTranslation(acceptLanguage) || 'en-GB'; 

		// Translate the content (or strip the _t() tags)
		const translatedContent = this.translator.autoTranslate(fileContent, availableLanguageCode); 

		// Return the file.
		return new Response(indexFile); 
		// Return the translated content.
		return new Response(translatedContent, { 
			headers: { 
				'Content-Type': 'text/html; charset=utf-8', 
			}, 
		}); 
	}

	@Get('/:asset')
	public async get(@Params('asset') asset: string) {
		const assetPath = resolve(this.basePath, asset.replaceAll('..', '')); // Prevent directory traversal attacks.
		if (!assetPath.startsWith(this.basePath)) { // Ensure the asset path is within the public directory, ensuring there are no additional directory traversal attacks.
			return new Response('Forbidden', { status: 403 });
		}
		const assetFile = file(assetPath);
		if (!await assetFile.exists()) {
			return new Response('Not Found', { status: 404 });
		}
		return new Response(assetFile);
	}
}

As you can see, instead of returning the indexFile directly, we read the file content as text, then use the autoTranslate method to translate the content based on the user's locale, which we can get from the Accept-Language header or a query parameter. We also added a locale query parameter to allow the user to override the locale, this is useful for testing purposes, but in a real application, you would want to use the Accept-Language header to determine the user's locale.

Summary

With all these changes applied, you can now start your application again, and see the translations in action, and you can add the ?locale=es-ES or ?locale=pt-PT query parameter to the URL to see the translations in action.

The final change will include the @sodacore/discord plugin, which we will cover in the next section.

Released under the Apache-2.0 License.