Skip to content

@sodacore/i18n

The i18n plugin for the Sodacore framework provides a simple way to add internationalization (i18n) support to your application. It allows you to manage translations for different languages and switch between them easily.

Installation

To install the plugin, simply install it via bun:

bash
bun install @sodacore/i18n

And within your main.ts file you can use the plugin like so:

typescript
import { Application } from '@sodacore/core';
import I18nPlugin from '@sodacore/i18n';

const app = new Application({
	autowire: true,
});

app.use(new I18nPlugin({
	defaultLocale: 'en-GB',
	enableFileLookup: true,
	fileTranslationsPath: './locales',
	overrideGetParam: 'lang',
}));

app.start().catch(console.error);

Configuration

The i18n plugin has the following configuration options:

ts
export type IConfig = {
	defaultLocale?: string,			// The default locale to use, defaults to 'en-GB'.
	enableFileLookup?: boolean,		// Whether to enable file-based translation lookup.
	fileTranslationsPath?: string,	// The path to the directory containing translation files.
	overrideGetParam?: string,		// The query parameter to use for overriding the locale (useful for debugging when using the TranslateTransform).
};

NOTE

If you define a defaultLocale it assumes that the keys in your translation files are the original text, e.g. "Hello": "Hello" and for French: "Hello": "Bonjour", this allows you to fallback to the key instead.

Lookups

The way the i18n plugin works is by creating lookups, that can be created to lookup translations from different sources. The plugin comes with a FileLookup that can be enabled within the configuration, which will look for translation files within the specified directory.

This then expects a directory with files formatted like so: <locale>.json, e.g. en-GB.json or fr-FR.json.

Each file should contain a JSON object with key-value pairs for translations, for example:

json
{
	"greeting": "Hello",
	"farewell": "Goodbye"
}

TIP

This design allows you to use tokens for lookup, or you can simply use the original text as the key, e.g. "Hello": "Hello" and for French: "Hello": "Bonjour", depends if you set a defaultLocale.

Custom Lookups

You can also create your own custom lookups by implementing the ILookup interface:

ts
import { Lookup, type ILookup } from '@sodacore/i18n';

@Lookup()
export class CustomLookup implements ILookup {
	public async onInit() {
		// * Can be used to setup any initial state or configuration.
	}

	public async supports(locale: string): Promise<boolean> {
		// Should return whether this lookup will support the given locale.
		// This allows you to define which locales your lookup can handle if they are stored in different places.
	}

	public async getAvailableLanguages(): Promise<string[]> {
		// Should return a list of all available languages that this lookup knows.
		// This will be merged with the general list, so the plugin can pick the best one.
	}

	public async onTranslate(query: string, languageCode: string, fallback?: string): Promise<string> {
		// Should return the translated string for the given query and language code.
		// If no translation is found, it should return the fallback value if provided, otherwise the original query.
	}

	public async onTranslateMultiple(queries: IQuery[], languageCode: string): Promise<IQuery[]> {
		// Should return the translated strings for the given queries and language code.
		// Each query in the input array should be updated with its corresponding translation.
		// It will attempt to translate each query, and if a translation is not found, it will set the translated field to the orignal.
	}
}

TIP

*: onInit lifecycle method is called once during application startup.

IQuery type

The IQuery type is used when translating multiple queries at once, it has the following shape:

typescript
export type IQuery = {
	original: string,
	value: string,
	translated?: string,
};

Usage

To use the translation features there are a few supported ways.

On-Demand Translation

You can use the I18nProvider to perform on-demand translations.

typescript
import { Controller, Get } from '@sodacore/http';
import { I18nProvider } from '@sodacore/i18n';
import { Inject } from '@sodacore/di';

@Controller('/greet')
export class GreetController {
	@Inject() private i18n!: I18nProvider;

	@Get('/')
	public async greet() {
		const greeting = await this.i18n.translate('greeting', 'en-GB');
		return { message: greeting };
	}
}

Translate transformer

You can also use the TranslateTransform to automatically translate responses based on the request's locale (only supports @sodacore/http package).

typescript
import { Controller, Get, Transform } from '@sodacore/http';
import { TranslateTransform } from '@sodacore/i18n';

@Controller('/greet')
export class GreetController {

	@Get('/')
	@Transform(TranslateTransform)
	public async reverseString() {
		return `
			_t(Hello), World!
		`;
	}
}

As you can see, the TranslateTransform will look for any strings that are wrapped in _t(...) and attempt to translate them based on the request's locale, which can be overridden by the query parameter defined in the config (e.g. ?lang=fr-FR).

WARNING

The transform will automatically strip the _t(...) wrapper and return the translated string or original if no translation is found.

You can see this in action in the Demo.

Constants

The i18n package also exports a few constants that can be used throughout your application:

  • Constants.REGEX_TRANSLATION_TAG - The regex used to find translation tags in strings (e.g. _t(...)).
  • Constants.REGEX_TRANSLATION_TAG_QUERY - The regex used to extract the query from a translation tag (e.g. _t(Hello) -> Hello).

Utilities

The i18n package also provides a few utility functions that can be used to help with translations:

  • parseLocaleHeader(acceptedLanguages: string): IParsedLanguage[] - Parses an Accept-Language header string and returns an array of parsed language codes with their quality values.
  • scoreLocaleMatch(requested: IParsedLanguage[], supported: string): number - Scores how well a supported locale matches the requested locales based on quality values.
  • getBestLocale(acceptedLanguageHeader: string, availableLanguages: string[]): string - Determines the best matching locale from the accepted languages and available languages, uses scoreLocaleMatch internally.

Type: IParsedLanguage

typescript
export type IParsedLanguage = {
	locale: string, // e.g. 'en-GB'
	quality: number, // q value
	language: string, // e.g. 'en'
	region?: string, // e.g. 'GB'
};

Released under the Apache-2.0 License.