npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@sveltevietnam/i18n

v0.2.3

Published

composable and minimal i18n solution for Svelte & SvelteKit

Readme

@sveltevietnam/i18n

MIT npm.badge

A homebrew i18n solution for Svelte applications, built for type-safety, composability, and optimization, by leveraging:

See more in the "Internationalisation with Remote Function — A Proof of Concept" blog post.

Changelog

See CHANGELOG.md.

Introduction

This package allows the following pattern:

messages:
  just_string: Goodbye!
  some_html: '<strong>Welcome!</strong>'
  string_with_params: Hello {{name}}!
  nested:
    message: This is a nested message.
  imports:
    from_local:
      '@import': ./path/to/another/locale.yaml
    from_alias:
      '@import': '$lib/components/my-component/locales/lang.yaml'
    from_package:
      '@import': '@design-system/another-component/locales/lang.yaml'
<script>
	import { T } from '@sveltevietnam/i18n';
</script>

<!-- type safe -->
<T key="some_html" />
<T key="string_with_params" params={{ name: 'world' }} />
<T key="nested.message" />

and more...

Getting Started

1. Install Dependencies

npm install --save-dev @sveltevietnam/i18n
yarn add --dev @sveltevietnam/i18n
pnpm add -D @sveltevietnam/i18n

2. Use the Vite Plugin

import { sveltekit } from '@sveltejs/kit/vite';
import { i18n } from '@sveltevietnam/i18n/vite';
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [
		i18n({
			input: './src/lib/i18n/locales', // where your locale files live (en.yaml, vi.yaml, etc)
			output: './src/lib/i18n/generated', // where generated modules live
		}),
		sveltekit(),
	],
});

3. Configure Svelte & SvelteKit

Turn on experimental Async Svelte and SvelteKit remote function, if your setup allows so

import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
export default {
	preprocess: vitePreprocess(),
	compilerOptions: {
		experimental: {
			async: true,
		},
	},
	kit: {
		adapter: adapter(),
		experimental: {
			remoteFunctions: true,
		},
	},
};

[!NOTE] If you cannot use above experimental features, you can still use the package in "static" mode. See Remote vs Static Mode for details.

4. Define Locales

Define your locale files as <lang>.yaml under the input folder you specified in the Vite config, for example:

lib/
	i18n/
		locales/			<- input folder
			en.yaml			<- messages in English
			vi.yaml			<- messages in Vietnamese
		generated/	  <- output folder

...with format as seen in Introduction:

messages: ...

5. Configure I18N Provider

Configure i18n provider where appropriate, e.g., in src/routes/+layout.svelte:

<script lang="ts">
	import { Provider } from '@sveltevietnam/i18n';
	import { type Language } from '@sveltevietnam/i18n/generated';

	let lang = $state<Language>('vi');

	// manage lang state as you see fit
	// change to lang will propagate to translations automatically
</script>

<Provider {lang} remote="prerender">... your app here ...</Provider>

[!NOTE] See "Choose your Remote Function" for more details on how to specify the remote prop.

Translating Messages

There are several ways to translate your messages, and using T component is recommended whenever possible.

Using T Component

<script lang="ts">
	import { T } from '@sveltevietnam/i18n';
</script>

<T key="key.to.your.message" />

[!NOTE] Typing should be inferred. If your message specs contains parameters, additional params prop is expected.

Using t function from context

Internally, T component uses a t function from the i18n context, which you also have access to:

<script lang="ts">
	import { Context } from '@sveltevietnam/i18n';

	const { t } = Context.get();
</script>

{await t({ key: 'key.to.your.message' })}

The interface of t input mirrors that of T prop:

[!IMPORTANT] A few things to note:

  • In the default "remote" mode, t is asynchronous (calls remote function internally).
  • You will need to handle html string yourself, i.e using {@html ...}.

This is helpful when you are translating non-html messages for attributes, e.g.,

<dialog aria-label={await t({ key: 'key.to.your.message' })}></dialog>
<!-- or -->
<input placeholder={await t({ key: 'key.to.your.message' })} />

In case of dialog in the above example, alternatively, you can still use T component with aria-labelledby T component can sill be used.

Overriding Lang

Both T component and t function accept an optional lang parameter in case you need to override the current language from context.

<T key="key.to.your.message" lang="en" />
{await t({ key: 'key.to.your.message', lang: 'en' })}

Using Static Messages

Sometimes translation happens outside of i18n context for T and t, in which case you can import and use the generated static messages directly:

// in server, or some isolated modules
import * as m from '@sveltevietnam/i18n/generated/messages';
const message = m['key.to.your.message'];

[!NOTE] When importing static messages, use the wildcard import syntax (* as m) to better facilitate tree-shaking.

Using Remote Functions

In "remote" mode, t internally calls the generated remote function, which you can also import and use directly:

import { query, prerender } from '@sveltevietnam/i18n/generated/t.remote';

const translated = await query({
	// or prerender
	lang: 'en',
	key: 'string_with_params',
	params: { name: 'world' }, // inferred from key
});

Similar to t, you will need to handle html string yourself.

Choose your Remote Function

In "remote" mode, Provider, T, and t accept a remote parameter (required on provider) that specifies which remote function to fetch translation from. remote can be:

  • prerender: uses SvelteKit prerender via the generated prerender function at <output-dir>/t.remote.js. This is usually what you want if you've turned on prerendering for your page(s),
  • query: uses SvelteKit query.batch via the generated query function at <output-dir>/t.remote.js. This can batch multiple translation requests but may not be able to utilize cache,
  • your own: import yours from some .remote.{js,ts} and pass it here to provide an implementation that works for your setup. The generated modules are at your disposal.

Global Remote Function

<script lang="ts">
	import { Provider } from '@sveltevietnam/i18n';
	let lang = $state<Language>('vi');
</script>

<Provider {lang} remote="prerender">
	<!--
		unless specified otherwise, all children T and t will fetch from
		import('@sveltevietnam/i18n/generated/t.remote').prerender
	-->
</Provider>

[!NOTE] Context may also be set programmatically via the imported Context class, i.e Context.set.

Remote Function per Translation

<script lang="ts">
	import { T, Context } from '@sveltevietnam/i18n';
	const { t } = Context.get();
</script>

<!-- these use whatever remote function specified in provider -->
<T key="key.to.message" />
{await t({ key: 'key.to.message' })}

<!-- these overrides to use prerender function -->
<T key="key.to.message" remote="query" />
{await t({ key: 'key.to.message', remote: 'query' })}

Remote vs Static Mode

Remote Mode

By default, the package builds in "remote" mode and assumes you have enabled experimental features as discussed in Configure Svelte & SvelteKit. This leverages Svelte & SvelteKit capabilities for optimization. i.e translations are lazily fetched only when and where needed.

Static Mode

In cases, however, you don't have access to those features, the package can still run in "static" mode. This also helps if your project is not using SvelteKit (assuming Svelte+Vite).

Start by passing mode: 'static' to vite plugin config:

// ...truncated...
i18n({
	mode: 'static',
});
// ...truncated...

The interface of T component changes in static mode. You will need to pass in static messages from the generated modules instead:

<script lang="ts">
	import { T } from '@sveltevietnam/i18n';
	import * as m from '@sveltevietnam/i18n/generated/messages'; // or where you defined as output
</script>

<T message={m['key.to.your.message']} />

Similar restrictions apply to t function from context.

HTML Sanitization

Both T component and t function perform HTML sanitization by default, using sanitize-html, and will strip all attributes except id, class and data-*, or href, target, and rel for a anchor tags.

Customization is permissible via the sanitize parameter to Provider, T component, or t function.

<Provider sanitize={customSanitize}>...</Provider>
<T key="key.to.message" sanitize={customSanitize} />
{t({ key: 'key.to.message', sanitize: customSanitize })}

Output Modules

When running the Vite plugin, the output option specifies the directory that will host the following modules:

<output>/				<- e.g. ./src/lib/i18n/generated
	messages/
    index.js
		[lang].js
	i18n.d.ts
	constants.js
  t.remote.js
  • messages/[lang].js: each contain language-specific "message targets".
  • messages/index.js: contains static "messages". This is the module that @sveltevietnam/i18n/generated/messages aliases to. See Using Static Messages for some more information.
  • constants.js: contains some helpful constants like languages, keys, current mode, can be imported as @sveltevietnam/i18n/generated/constants. See Using Remote Functions for some more information.
  • i18n.d.ts: module augmentation for on-demand typing support.
  • t.remote.js: contains the remote functions for translation, can be imported from @sveltevietnam/i18n/generated/t.remote.

Although importing from @sveltevietnam/i18n/generated is most convenient, you can also opt to import directly from the output directory. For example, in SvelteKit, that may look like:

import { langs } from '$lib/i18n/generated/constants';
import * as m from '$lib/i18n/generated/messages';
import { query } from '$lib/i18n/generated/t.remote';

This is helpful if you are reusing code in non-vite context, e.g. in Playwright tests.

Composability

Locale files can import other locale files via the special @import directive, as seen in Introduction. This allows you to break down your locale files into smaller, manageable pieces, and even reuse locale files from other packages.

[!IMPORTANT] The @sveltevietnam/i18n Vite plugin, however, expects a single entry locale file per language. Using multiple instances of the plugin is feasible but not recommended as type augmentation may not work as expected. If you do this, proceed with caution.

Import aliases inherit from other vite plugins including $lib from SvelteKit or others you've configured in svelte.config.js. Customization is possible via option to the i18n vite plugin.

Why YAML?

This package was originally built to facilitate internationalization for the sveltevietnam.dev site, which has not yet require complex features such as pluralization. As such, YAML was chosen for simplicity, i.e easy to read, easy to parse, compared to more sophisticated formats such as ICU or GNU gettext.

This may change in future releases. Contributions and discussions are welcome.

Using JSON

You can customize the parser to accept JSON files instead:

i18n({
	input: './src/lib/i18n/locales',
	output: './src/lib/i18n/generated',
	parseOptions: {
		deserializer: {
			parse: ({ content }) => JSON.parse(content),
		},
	},
});

Feedback and contributions are welcome. Start at CONTRIBUTING.md.