Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Translations #1274

Open
Rich-Harris opened this issue Apr 29, 2021 · 147 comments
Open

Translations #1274

Rich-Harris opened this issue Apr 29, 2021 · 147 comments
Labels
feature / enhancement New feature or request p1-important SvelteKit cannot be used by a large number of people, basic functionality is missing, etc. size:large significant feature with tricky design questions and multi-day implementation
Milestone

Comments

@Rich-Harris
Copy link
Member

Is your feature request related to a problem? Please describe.
Translations are largely an unsolved problem. There are good libraries out there, like svelte-i18n and svelte-intl-precompile, but because they are not tightly integrated with an app framework, there are drawbacks in existing solutions:

  • Usage is somewhat verbose (e.g. {$_("awesome", { values: { name: "svelte-i18n" } })}) from the svelte-i18n docs
  • Loading translations is generally a bit awkward
  • Messages are untyped

Without getting too far into the weeds of implementation, what follows is a sketch for what I think the developer experience could and should look like for using translations in SvelteKit. It builds upon the aforementioned prior art (using ICU MessageFormat, precompiling translations etc) while leveraging SvelteKit's unique position as an opinionated app framework. It follows the discussion in #553, but doesn't address issues like picking a locale or handling canonical/alternate URLs, which overlap with translations but can be designed and implemented separately to a large extent.

Describe the solution you'd like
In the translations directory, we have a series of [language].json files with ICU MessageFormat strings:

// en.json
{
  "brand": {
    "name": "SvelteKit",
    "tagline": "The fastest way to build Svelte apps",
    "description": "SvelteKit is the official Svelte application framework"
  },
  "greeting": "Welcome!",
  "clicked_n_times": "{n, plural,=0 {Click the button} =1 {Clicked once} other {Clicked {n} times}}"
}

Regional dialects inherit from the base language:

// en-AU.json
{
  "greeting": "G'day mate!"
}

These files are human-editable, but could also be manipulated by tooling. For example, the SvelteKit CLI could provide an editor along the lines of this one or this one out of the box.

SvelteKit watches these files, and populates an ambient.d.ts file that lives... somewhere (but is picked up by a default SvelteKit installation) with types based on the actual translations that are present (using the default locale, which could be specified in svelte.config.cjs, for the example strings):

declare module '$app/i18n' {
  import { Readable } from 'svelte/store';

  /**
   * A dictionary of translations generated from translations/*.json
   */
  export const t: Readable<{
    brand: {
      name: 'SvelteKit';
      tagline: 'The fastest way to build Svelte apps';
      description: 'SvelteKit is the official Svelte application framework'
    };
    greeting: 'Hello!',
    clicked_n_times: (n: number) => string;
  }>;
}

As well as generating the types, SvelteKit precompiles the strings into a module, similar to svelte-intl-precompile:

export default {
  brand: {
    name: 'SvelteKit',
    tagline: 'The fastest way to build Svelte apps',
    description: 'SvelteKit is the official Svelte application framework'
  },
  greeting: 'Hello!',
  clicked_n_times: (n) => n === 0 ? 'Click the button' : n === 1 ? 'Clicked once' : `Clicked ${n} times`
};

In a large enough app this module could get very large. We could theoretically use static analysis to determine which translations are used by which routes, and only load the translations needed for the current route, but I don't think that's necessary as a starting point.

This module is using for server-rendering, and is also loaded by the client runtime to populate the t store. (Because it's a store, we can change the language without a full-page reload, if necessary. That might be overkill, but if Next can do it then we should too!)

A relatively unique thing about this approach is that we get typing and autocompletion:

image

This extends to parameters (which I think should be positional), for messages that need them:

image

Because everything is just JSON files, it's trivial to build tooling that can e.g. identify untranslated phrases for a given language. I don't know what common translation workflows look like, but it would presumably be possible to convert between the file format used here and the output of translation software that uses ICU.

Describe alternatives you've considered

  • Leaving it to userland. I think i18n is too central to be treated as not-our-problem, and the potential ergonomic benefits from having it in the framework are considerable. (Our solution need not preclude userland solutions, if people have strong preferences)
  • Using a different message format. Fluent and Banana both came up in i18n brainstorming #553. We could theoretically support multiple formats but it would be preferable to pick one that most people are happy with

How important is this feature to you?
It's time. Though we need to solve the problem of selecting a locale before we can make much of a start on this; will open an issue in due course.

@babakfp

This comment has been minimized.

@pago
Copy link

pago commented Apr 30, 2021

Could you clarify this part a little bit?

This extends to parameters (which I think should be positional), for messages that need them

The part I'm wondering about is whether that could result in changes to the translation messages invalidating existing code due to a shift of parameter order.
Apart from that potential risk I also feel like having an object with keys and values might actually add to the readability of the code.

Quick example:

$t.clicked_n_times(n);
// compared to
$t.clicked({times: n});

Everything else sounds outstanding and I can't wait to start using it. Thank you for tackling this issue.

@tempo22
Copy link

tempo22 commented Apr 30, 2021

Hi,

Allow me to jump into the discussion here. I'm planning a new project and a robust i18n is a must-have on it.

I have worked in the past with Drupal and Wordpress websites, both using po file format. It seems to be made for this purpose. We may use JSON but it would be nice to see how they handle the pluralization and other translation workflows.

But as you said, selecting a locale is an important step that should be done before loading translation.

@floratmin
Copy link

I have just written something to extract translation functions calls into .po files. Currently, I am integrating it into a rollup plugin. My idea is to use the tooling of gettext for managing the translation lifecycle. I think that we could even use ICU messageformat strings inside .po files. The advantage of .po files is that it is easy to provide a lot of extra context, which is often very important for translators. You can check my repository out at gettext-extractor-svelte.

Then there is the package gettext-to-messageformat which converts .po files with gettext format to messageformat. If instead ICU messageformat strings are used in .po files, the conversion is trivial. An option to use json with ICU messageformat instead of .po files should also be easy to implement, but for this, I have to investigate how comments are provided to translators.

Then the messageformat project can compile the translated strings into pure functions which can then be injected when building the application and replace the translation function and function calls.

The actual translation function could be changed according to the needs of the project. Simple projects would only go with the message string, while other projects could provide unique context and detailed comments for each message string.

@zwergius
Copy link

Hey,

Here is what I use for selecting the initial language

import { supportedLanguages } from '$lib/constants';
const DOCUMENT_REGEX = /^([^.?#@]+)?([?#](.+)?)?$/;

export function getContext({ headers, path }) {
  const isDocument = DOCUMENT_REGEX.test(path);
  if (!isDocument) return;

  let language = path.split('/')[1];
  // language not in url
  if (supportedLanguages.indexOf(language) === -1) {
    language = supportedLanguages[0];

    if (headers['accept-language']) {
      const headerLang = headers['accept-language'].split(',')[0].trim().slice(0, 2);

      if (headerLang && headerLang.length > 1) {
        if (supportedLanguages.indexOf(headerLang) !== -1) language = headerLang;
      }
    }
  }

  return { language };
}

export function getSession({ context }) {
  const { language } = context;
  return { language };
}

export async function handle({ request, render }) {
  const rendered = await render(request);

  if (rendered.headers['content-type'] === 'text/html') {
    const { language } = request.context;

    return {
      ...rendered,
      body: rendered.body.replace('%lang%', language)
    };
  }

  return rendered;
}

I use the accept-language header and then in my src/routes/index.svelte I have

<script context="module">
  export async function load({ session }) {
    return {
      status: 303,
      redirect: `/${session.language}`
    };
  }
</script>

I then in src/routes/[lang]/$layout.svelte check if the lang param is supported and if not 404

I than have a static yaml file that holds all translations

This works fairly well for static pages which is what I do mostly... I put this together after reading a whole bunch of issues, initially trying svelte-i18n which I then decided was overkill...

@kobejean
Copy link

kobejean commented Apr 30, 2021

I've used svelte-i18n before and one big reason I prefer the $t('text') format over $t.text is that it is supported in this vs code plugin: i18n-ally . It's a great way to manage your translations, have translation reviews/feedback and it even allows machine translation.



@babakfp
Copy link

babakfp commented May 2, 2021

See what is wrong with svelte-i18n

An example from svelte-i18n

<script>
  import { _ } from 'svelte-i18n'
</script>

<h1>{$_('page.home.title')}</h1>

<nav>
  <a>{$_('page.home.nav', { default: 'Home' })}</a>
  <a>{$_('page.about.nav', { default: 'About' })}</a>
  <a>{$_('page.contact.nav', { default: 'Contact' })}</a>
</nav>

I think this is a 100% bull sheet. Look at all of the work that you need to do, just to set a default value for a translateable value. You are not done, you also need to create a .json file and write you all strings there. Also, you need to pick a name for your translatable string as a reference. for example:

// en.json
{
  "hello_world": "Hello, World!"
}

So what is wrong with svelte-i18n?

  1. It's a lot of wooooooooooooooooooork.
  2. It's unnecessarily a lot of wooooooooooooooooooork.
  3. It's just nonsense.
  4. The $ character is here too!!. I hate this, but no worries, because of my PHP background experience I'm immune 😜

I know that this was the best possible way for them to create this library. I know that they put in a lot of work and they did what they were possible of doing it.

I explained what I expect from a layout system here and nothing, we are going to experience the same problems in the SvelteKit too. Now I want to explain what I expect from a translation system because I need to say what I think about it and how I think it needs to be solved because if I don't, I think it's gonna be really bad 😞.

Also, I want to say that I get banned (2 of my accounts) from the community discord group. Admins were like "Nah, I don't like this guy, clicking on the ban button". (The person that reading this, if you get banned from the discord channel, knows that there was no problem with you). If you didn't like what I explain here, you can stick with this subject that 2 of my accounts get banned, instead of saying what you don't agree with 😄. I wanted to complain about it with Mr. Rich Harris but, it seems he is so busy.
Oh God, now I feel like I complaining like a baby 😆

Let's keep going

What I expect from a translation system?

Exactly the same that we are using in the current tools. For example Wordpress:

<?php echo __('Hello, World!'); ?>

I sawed the same syntax in Laravel but I don't have a lot of information about it. Any Laravel developers here to explain to us about how it worlds and how was the experience?

With this, you don't need to write all of those svelte-i18n junk you sowed above!.

How it works?

Automatically the strings inside the functions get write inside the .pot file. You insert the .pot file inside a software called Poedit. You select the language and then Poedit will show you the all translatable strings. Translate the strings that you see and much more features, that will make life easier for you, rather than creating a .json file and blah blah blah.
screenshot-help@2x

You are gonna have 3 files

  • .pot: Your all strings live here (input). translatable-strings.pot.
  • .po: Translated string (output). [language].po.
  • .mo Same as the .po but the file isn't editable!. [language].mo.
You are confused?

You are just gonna write your string inside the __() function. All of the other work going to take care of by the system it selves.

Pros

  • Much less code.
  • Much fewer problems.
  • Easier life ;)
  • A software to help you easily edit your strings. Click on a button and google translate will translate it all for you!.
  • Don't need to learn new stuff.
  • It is the most known syntax for translations = {Count how much people uses Wordpress} + {Count how much people uses Laravel}.
  • We do not need to reinvent the wheel!.
  • $t(), oh my brain 😟
  • This will provide the solution for: Translations #1274 (comment)

Cons

  • If you edit the initial / the source string, you will lose all the translations related to that. For example: before: Hello world and after: Hello World.
  • The Hello string and the hello string are treated as different strings.

Why I hate $t()

  • $t(), oh my brain 😟
  • why not just use the initial string as the key? Use the initial string Welcome instead of greeting. Translations #1274 (comment)
  • Don't like the $
  • Why reinvent the wheel!.

@kvetoslavnovak
Copy link
Contributor

Big THANK YOU to all dealing with the translation topic.
i18n is a huge “must” for all non-US regions. E.g. for us in EU especially.

@Kapsonfire-DE
Copy link
Contributor

@babakfp i disagree with the point of you have to define keys and default translations
you can directly use

{$_('Won {n, plural, =0 {no awards} one {# award} other {# awards}}', { values: { n: 2}})}

no need to define a key and define a default translation - same behaviour as gettext

@tempo22
Copy link

tempo22 commented May 2, 2021

@babakfp i disagree with the point of you have to define keys and default translations
you can directly use

{$_('Won {n, plural, =0 {no awards} one {# award} other {# awards}}', { values: { n: 2}})}

no need to define a key and define a default translation - same behaviour as gettext

The plural handling in your example will create a mess where the sentence order is totally different depending on the language:
I won 1 award -> J'ai gagné 1 prix
I won 2 awards -> J'ai gagné 2 prix
I won no awards -> Je n'ai gagné aucun prix

In PO files, the plural is handled with the following syntax

msgid "I won {n} trip"
msgid_plural "I won {n} trips"
msgstr[0] "J'ai gagné {n} voyage"
msgstr[1] "J'ai gagné {n} voyages"

The number of plural variant depend on the language, read this for more informations

Using a more complex example:

{#if n == 0 }
   {$t('Hello {firstname}, you have no new messages', {firstname: user.firstname})}
{:else}
   {$t('Hello {firstname}, you have {n} new message', 'Hello {firstname}, you have {n} new message', n, {n: n, firstname: user.firstname})}
{/if}

we could have the translation function with the following definition:

function t(msgid: string, replacements?: object): string
function t(msgid: string, msgidPlural: string, count: number, replacements?: object): string

@Kapsonfire-DE
Copy link
Contributor

@tempo22 it's the ICU MessageFormat
You gonna create a fitting translation string for french in your translation files.

@floratmin
Copy link

@Kapsonfire-DE ICU messageformat can get very similar to gettext:

{
  $_(
    `{
      n, plural, 
      =0 {I won no awards}
      one {I won one award} 
      other {I won # awards}
    }`,
   { n: 2 }
  )
}

This is also the recommended way for writing messageformat strings. Otherwise, an external translator can get confused.

I think that some have the perspective of translating their app/page on their own or in house. But we should not forget, that there are also many use cases where we need professional translations and where we need to provide as much context to the translators as possible. Because of this, I like gettext (.po, .pot, .mo) very much. There are a lot of editors, there is an established workflow and many translators know this system.

But messageformat is sometimes more flexible, especially you can use the Intl functions directly, which avoids a lot of bloat. And because messageformat are strings we can put these strings also into .po files and use the gettext workflow and infrastructure. If the context is not needed, we can put these strings still into JSON or YAML files and use simpler functions.

An other case is when there is already a project which is based on gettext. Then it can make very much sense to use gettext instead of messageformat, because all translations from before can be reused. So it would be nice if there is some flexibility.

@johnnysprinkles
Copy link
Contributor

More important than the specific syntax is that we understand the typical flow of translations... I can describe how it works at Google for example, which is probably similar to other large tech companies.

Translations are a continuous and ongoing process. Commits land constantly and most of them have new strings, then in a periodic process we extract all the untranslated strings and send them off for translation. Google and Amazon I know have "translation consoles" that manage it for you. So say weekly you pull the strings and send them off, then they come back a week or more later, you check that data in and it's available after your next deployment.

One thing to note is that, if you're using human translators, you must have a description. The English text alone isn't enough to get a good translation. Not sure how this changes as we move more and more to machine translation. Early on at Google I worked on a GWT project where the translations were declared in code like this:

<ui:msg description="Greeting">Hello, world</ui:msg>

But most stuff at Google uses goog.getMsg(), i.e.

@desc Greeting
const MSG_GREETING = goog.getMsg('Hello, world');

What Rich is describing with a key and all versions of the text including the primary English version located elsewhere in a data file, that sounds good for the "glossary of well known terms you want to use across the site" scenario, but the vast majority of strings we use are translated per instance, since their meaning and context varies. Trying to share strings to save on translation costs would just slow down development too much. Maybe smaller projects have different priorities of course though.

So I'd push for the English text (or whatever the primary language is) and the description located in the Svelte component itself. That way it works immediately and works even if you lose your translation data, and you don't have the Svelte component author having to hand fill-in a data file for one language while all the other languages come from an automated process.

@Rich-Harris
Copy link
Member Author

Thanks @johnnysprinkles — I haven't experienced this sort of translation workflow so that's useful.

I'm not wholly convinced that including the primary language in the component alongside a description is ideal though:

  • Parsing out the language from the component source adds significant complexity. The case where you have a piece of markup like <ui:msg description="Greeting">Hello, world</ui:msg> is one thing, but if you want to be able to refer to translations in JavaScript as well, you have to have some way of associating descriptions with variables, and the whole thing has to be amenable to static analysis. This would likely involve non-trivial changes to Svelte itself, even if we could come up with a design that made sense
  • <h1>{t.greeting}</h1> is nicer than <h1><ui:msg description="Greeting">Hello, world</ui:msg></h1>. The latter adds a lot of distracting visual noise that I probably don't want when I'm building a UI
  • It forces developers to be copywriters. Perhaps 'Hello, world' is the wrong tone of voice?
  • It's brittle — if the text + description is essentially a 'key' for other translations, then we can't edit the primary language text, even to remove typos. Keys are a lot more stable
  • It's harder to keep the original primary language strings out of bundles for all languages unless you have sophisticated tooling
  • It forces you to use descriptions even if they're unnecessary for your workflow, because otherwise two strings that are the same in one language but context-dependent in another rely on the description for disambiguation:

    An example that I love to use is the term “Get started.” We use that in our products in a lot of places and, in American English, it’s pretty standard. It’s so understandable that people don’t even think of the fact that it can be used in three or four ways. It could be a call to action on a button. Like, “Get started. Click here.” It could be the title of the page that’s showing how you get started. It can be the name of a file: a Get Started guide PDF. All of those instances need to be translated differently in most other languages.

So that leaves the question 'how do we associate descriptions with keys'? I think that's actually quite straightforward, we just have a descriptions.json or whatever in the same places as the translations themselves:

// en.json
{
  "brand": {
    "name": "SvelteKit",
    "tagline": "The fastest way to build Svelte apps",
    "description": "SvelteKit is the official Svelte application framework"
  },
  "greeting": "Welcome!",
  "clicked_n_times": "{n, plural,=0 {Click the button} =1 {Clicked once} other {Clicked {n} times}}"
}
// description.json
{
  "brand": {
    "name": "The name of the project",
    "tagline": "The tagline that appears below the logo on the home page",
    "description": "A description of SvelteKit that appears in search engine results"
  },
  "greeting": "A message enthusiastically welcoming the user",
  "clicked_n_times": "The number of times the user has clicked a button"
}

I'm envisaging that we'd have some way to convert between this format and those used by common translation workflows (.po as mentioned above, etc), so that the descriptions are colocated with the primary language as far as translators are concerned. These descriptions would also become part of the type signature:

declare module '$app/i18n' {
  import { Readable } from 'svelte/store';

  /**
   * A dictionary of translations generated from translations/*.json
   */
  export const t: Readable<{
    brand: {
      /** The name of the project */
      name: 'SvelteKit';
      /** The tagline that appears below the logo on the home page */
      tagline: 'The fastest way to build Svelte apps';
      /** A description of SvelteKit that appears in search engine results */
      description: 'SvelteKit is the official Svelte application framework'
    };
    /** A message enthusiastically welcoming the user */
    greeting: 'Hello!',
    /** The number of times the user has clicked a button */
    clicked_n_times: (n: number) => string;
  }>;
}

@babakfp
Copy link

babakfp commented May 12, 2021

Hi
I have a question, how long it will take to implement it in this way? There are pros and cons of doing something right? What are the pros of doing it this way ".pot, .po, .mo"?

  • It's going to be implemented sooner and easier.
    I'm not an expert but, I think that the hardest part, isn't the developing journey. The hard part is deciding on how the result gonna be like. It's really hard and frustrating to decide on this kinda stuff, especially if you don't have real experiments with it. You don't know what features gonna be needed, there are going to be tons of issues and etc.
  • in comparison, there are going to be tons of tutorials, visual tools, and tons of people that know how to work with the .pot way.
  • What about people that don't know how to write code? I bet on my life that they are not gonna like write code because they don't want to waste a lot of time, be frustrated and finally work with a text editor that doesn't provide you features like a visual translation tool does.
    There is no reason to reinvent the wheel, so please don't😨😅. Also, Thank you all for doing all that insane work💜

unrelated stuff

Is it possible to know about those tools and features status? like a daily blog or something? Maybe a Discord channel that just the maintainers can send voices, details, pulls, and etc? By the way, I created a fake account to join the Discord server😂 (feels like admitting the crimes to the cops😄). Thanks again for banning me👏, "You are the best" MR. pngwn#8431(if I'm not mistaken)😀

@Rich-Harris
Copy link
Member Author

@babakfp you're describing #1274 (comment), which is what I just responded to, except that you don't suggest a way to provide descriptions. Putting the primary language inline comes with a number of problems, and in terms of implementation/complexity, this...

It's going to be implemented sooner and easier

...couldn't be more wrong, I'm afraid. To reiterate, the expectation isn't that translators would edit JSON files directly. The goal would be to support existing translation workflows.

@Rich-Harris
Copy link
Member Author

Is it possible to know about those tools and features status?

Yep, by subscribing to this issue tracker

@johnnysprinkles
Copy link
Contributor

I mean this is just one perspective on it. We do optimize for frictionless development and happily translate the English string multiple times. We've probably translated 'Name' a thousand times, e.g.

['Name', 'Name of a virtual machine in the table column header']
['Name', 'Name of a virtual machine in the text input placeholder']
['Name', 'Name of a database table in the table column header']
['Name', 'Name of a database table in the text input placeholder']

Now that I look at it, the context probably isn't necessary, a specific type of "Name" should translate the same way whether it's in a table column header or a text input. But types of names certainly could translate differently, like the name of a person being different that the name of an entity. Sounds like we agree about that and a separate description.json sounds good to me.

One nice thing about having the English text and description hashed together as the key is that it's inherently reactive. Those are the two inputs that affect what the translation is, and if either change it needs to be retranslated (generally speaking).

I'm realizing now I might have muddled the issue with two totally different examples. In the GWT ui:msg case all the English text and descriptions are inline in the view template, meaning each instance of a string is translated separately. Nice for developer velocity but expensive, good for a big company with deep pockets for translation budget. You technically can share strings by copying the English text and description verbatim into multiple templates, because it'll make the same hash key, but that's not the norm. This whole example is pretty outdated, let's ignore it.

What we have with Closure Compiler may be more relevant. It's a constant that starts with MSG_ so we can sniff out which strings you're actually using at build time. I'm thinking about how hard it would be to graft this onto SvelteKit. We'd add a compilation pass either before or after Vite/Rollup. Of course I'd rather use native SvelteKit i18n if that turns out to work for us!

In a large enough app this module could get very large. We could theoretically use static analysis to determine which translations are used by which routes, and only load the translations needed for the current route, but I don't think that's necessary as a starting point.

I think it's pretty critical to have this split up per route from day 1, for those who are obsessed with the fastest possible initial page load and hydration.

So I guess I'd just say that Rich's proposal sounds good to me if we can make per-route translation string modules and if referencing a key with no primary language translation present fails the build right away.

@johnnysprinkles
Copy link
Contributor

Of course, the tooling that extracts the untranslated strings could pass along a key it derives however it wants. It could use the developer created name, or it could hash together the primary language/description pair. Maybe it could be a preference which way to go, but if set to the former there would need to be some way to manually invalidate, maybe a third parallel json file full of booleans.

Also regarding forcing developers to be copywriters, mostly what I've seen is a simple two stage pipeline where the initial version has the final English text, typically read off a mockup. I could certainly see also offering the option of a three stage pipeline where the initial text is blank or has placeholder text, then the copywriter fills in the English, then it goes off to the translators. Maybe that would be a feature that comes later.

@johnnysprinkles
Copy link
Contributor

johnnysprinkles commented May 12, 2021

Oh what about this -- two layers. A "stable" layer which is the hand written json you mentioned, and a fallback layer that's english/description hashcode based. So you'd start with your svelte file

{$t.click}

And add the stable English version:

// en.json
{
  click: 'Click',
}
// en.description.json
{
  click: 'Call to action, to do a mouse click'
}

But there's also an "unstable" data file that's keyed by hash:

// en.hashed.json
{}

To find $t.click in French, you'd first look in fr.json, then if not found there hash the English version with the description and use that key to look in fr.hashed.json. If not there it's untranslated and will be included in the next extract.

Once you get the French version back, you'd normally fill in fr.hashed.json, but you'd also have to option to put it directly in the more stable fr.json if you never want to worry about having to retranslate it.

This would cover the case of people who don't use tooling at all, maybe they have a Russian friend and a Japanese friend and just say "hey can you translate this handful of strings for me" and manually plug those into the xx.json files. But for the heavyweight production setups, the incoming translations would only ever go in an xx.hashed.json file. Your file would end up looking like:

// fr.hashed.json
{
  123: 'cliquer',
}

Later if you change the English to click: 'Click here' that would hash to 456 and you'd end up with:

// fr.hash.json
{
  123: 'cliquer', // obsolete
  456: 'cliquez ici', // or whatever, I don't speak French
}

@floratmin
Copy link

floratmin commented May 13, 2021

Hello @Rich-Harris, I am working on a rollup plugin that works on .svelte files (including typescript) but also on .ts or .js files. It should even work on .jsx files, but I did not test it. The advantage is, that this solution is not restricted to svelte/kit and that there is much more flexibility regarding different requirements. I want to share some thoughts/replies on your points:

  • I have already solved the parsing part of the svelte files with my gettext-extractor-svelte library. The whole translation functionality depends only on one javascript function, which can be used in all files anywhere The only point is to use the plugin before any other plugin if file references including line numbers in the extracted messages are needed. For special use cases, more than one translation function can be provided.
  • The name of the translation function or class can be freely chosen. Default would be t (and $t if a store should be used).
  • There are two separate parts of translating a web app. First, all the strings that are baked into the app, then the strings that come from some sort of database. This plugin focuses only on the first use case.
  • The whole point of putting these strings into source files is to have a reference with explanations for the translators while also having a clue for the programmer. If the tone of the language used for defining the message strings should be defined by somebody in a later step, then the extracted file containing the message strings can still be "translated" into a more refined version, which will replace the strings provided by the programmers in the build step. If there is a minor spelling error, it can be corrected only in the extracted file, which would prevent triggering a re-translation of all previous translations of this string. If somebody does not want to put actual text into their source code, they can also just use a key instead of text. The only drawback with this approach would be not to have key completion in the editor. But on the other side organizing and maintaining a hierarchy of keys can get also quite complicated. Therefore I prefer the actual text, just set it and forget it.
  • I replace all message strings with the translated and pre-compiled version directly in the source file when building. There is no overhead. Alternatively, we can also put all pre-compiled translation strings into a store and replacing the translation function call with keys referencing the value in the store. I have also still not looked into the possibility to serialize pre-compiled translation functions.
  • Nobody is forced to a special format. Except for the required text field, everything else is optional and the place of every property can be freely chosen. The text, context, and comments have to be actual strings. Maybe there could be functionality to define the context on a per-file basis if needed.
  • If needed custom translation functions or classes are possible. Even the message string parser could be changed.

I want to explain an example workflow with this approach:

Translation function calls could be:
import { t } from './t/translations';
// If the same message string is used multiple times, than it will be extracted into one msgid
t('Message String');
// add context to prevent messages with the same messagestring to be treated as same, if the same message
// string with the same context is used multiple times, than it will be extracted into one msgid/msgctxt
t('Message String', 'Message Context');
// add comment for the translator
t('Message String', 'Message Context', 'Message Comment');
// unneeded parts can be ommited
t('Message String 2', null, 'Message Comment');
// add messageformat string and supply properties for the string
t(
    'Message {FOO}', 
    'Message Context', 
    {comment: 'Message Comment', props: {FOO: 'Type of foo'}},
    {FOO: getFoo()}
);
// add language for live translations
t('Message String', 'Message Context', 'Message Comment 2', null, getLanguage());

The translation function can be defined in a flexible way. If needed the position of all arguments can be changed however it fits the translation function. If gettext should be used instead of messageformat we add an argument for textPlural to the translation function and set the useGettext option.

Let's say we have defined a translation function as:

import MessageFormat from '@messageformat/core';

const mf = new MessageFormat('en');

export function t(
    text: string | null, 
    id?: string | null, 
    comment?: {props?: Record<string,string>, comment: string, path: string} | string | null,
   props?: Record<string, any>
): string {
    const msg = mf.compile(text);
    return msg(props);
}

Than the function calls will be replaced with:

'Message String';
'Message String';
'Message String';
'Message String';
'Message String 2';
((d) => 'Message ' + d.FOO)({FOO: getFoo()});
'Message String';

In this case, the translation function would completely disappear.
Additional we would get the following .pot file (omitting the header):

#: src/App.js:3
msgid "Message String"

#. Message Comment
#. Message Comment 2
#: src/App.js:6, src/App.js:8, src/App.js:19
msgctxt "Message Context"
msgid "Message String"

#. Message Comment
#: src/App.js:10
msgid "Message String 2"

#. Message Comment
#. {FOO}: Type of foo
#: src/App.js:12
msgctxt "Message Context"
msgid "Message {FOO}"

If somebody does not need .pot files, I can also emit simple JSON objects connecting the function properties to the message strings. But then we can not use the context and comment properties. If there is a need for putting comments into a separate JSON file, this functionality can be easily added.

A more complicated example

// in App.svelte
<script lang="ts">
    import { t } from './t/translations';
    export let res: number;
</script>
// later
<p>
    {t(
        '{RES, plural, =0 {No foo} one {One foo} other {Some foo}}', 
        'Foo Context', 
        {comment: 'Foo Comment', props: {RES: 'Foo count'}},
        {RES: res}
    )}
</p>
// in Page.svelte
<script lang="ts">
    import { t } from './t/translations';
    export let d: Date;
    export let res: number;
</script>
// later
<p>
    {t(
        '{ D, date }',
        'Date Context', 
        'Date Comment',
        {D: d}
    )}
</p>
<p>
    {t(
        '{RES, plural, =0 {No bar} one {One bar} other {Some bar}}', 
        'Bar Context', 
        {comment: 'Bar Comment', props: {RES: 'Bar count'}},
        {RES: res}
    )}
</p>

This would extract the following .pot file:

#. Foo Comment
#. {RES}: Foo count
#: src/App.js:8
msgctxt "Foo Context"
msgid "{RES, plural, =0 {No foo} one {One foo} other {Some foo}}"

#. Date Comment
#: src/Page.js:9
msgctxt "Date Context"
msgid "{ D, date }"

#. Bar Comment
#. {RES}: Bar count
#: src/Page.js:17
msgctxt "Bar Context"
msgid "{RES, plural, =0 {No bar} one {One bar} other {Some bar}}"

If we assume that we have translation files for English and German and the translations for german in the file App.svelte are missing and falling back to English we get the following files:

// in App.svelte (engish and german version)
<script lang="ts">
    import { t } from './t/t2';
    export let res: number;
</script>
// later
<p>
    {((d) => t.plural(d.RES, 0, t.en , { "0": "No foo", one: "One foo", other: "Some foo" }))({RES: res})}
</p>
// in Page.svelte english version
<script lang="ts">
    import { t } from './t/t0';
    export let d: Date;
    export let res: number;
</script>
// later
<p>
    {((d) => t.date(d.D, "en"))({D: d})}
</p>
<p>
    {((d) => t.plural(d.RES, 0, t.en , { "0": "No bar", one: "One bar", other: "Some bar" }))({RES: res})}
</p>
// in Page.svelte german version
<script lang="ts">
    import { t } from './t/t1';
    export let d: Date;
    export let res: number;
</script>
// later
<p>
    {((d) => t.date(d.D, "de"))({D: d})}
</p>
<p>
    {(
        (d) => t.plural(d.RES, 0, t.de , { "0": "Kein bar", one: "Ein bar", other: "Ein paar bar" })
    )({RES: res})}
</p>
// ./t/t0.js
import {t as __0} from './t2.js';
import { date } from "@messageformat/runtime/lib/formatters";
export const t = {
    ...__0,
    date
};
// ./t/t1.js
import { plural } from "@messageformat/runtime";
import { de } from "@messageformat/runtime/lib/cardinals";
import { date } from "@messageformat/runtime/lib/formatters";

export const t = {
    plural,
    de,
    date
};
// ./t/t2.js
import { plural } from "@messageformat/runtime";
import { en } from "@messageformat/runtime/lib/cardinals";
export const t = {
    plural,
    en
};

I have almost finished my plugin up to this point. Some refactoring has to be done and some additional tests written. I think that I will publish the first beta version at the end of this week. I am also thinking to offer a very different approach where all translation strings per file will be in a store. This would give the possibility for 'live' translations. To put pre-compiled translation functions into JSON files, I have first to look into how to serialize them. This would give the possibility to live-load additional translation files on demand.

The workflow would be to leave everything in place when developing the application. Maybe there could be some functionality to load already translated strings into the app and/or provide some fake text per language. Then there is the extraction phase where all strings are extracted into .pot files and merged with the .po files from previous translations. When we have all translations back we build in the third step the actual translation functions and integrate them into our files. If necessary we can also define fallback languages to each language when some translation strings are missing.

The only missing link is now how to get the actual language at build time. For this, I would need svelte kits multi-language routing functionality.

@johnnysprinkles
Copy link
Contributor

I can say from experience it is nice for the development flow to be able to put your string data right inline. Rich thinks it adds visual noise but I tend to think it orients the code author, and is less cryptic.

I replace all message strings with the translated and pre-compiled version directly in the source file when building

Is this talking about both the pre-rendered HTML (as from utils/prerender) and the JS artifacts?

@johnnysprinkles
Copy link
Contributor

Actually never mind, prerender happens after JS building so of course it would apply the translations there.

@johnnysprinkles
Copy link
Contributor

We have a list of features, I wonder if something like this would be helpful https://docs.google.com/spreadsheets/d/1yN03V04RI8fBE9ppDkEf2-d9V3f4fHFwekteRNiJskU/edit#gid=0 If @floratmin is suggesting a kind of macro replacement, like t('greeting') swapped out with a string literal 'Hello' at build time, that would be incompatible with client-side language swapping.

@filsanet
Copy link

Google and Amazon I know have "translation consoles" that manage it for you. So say weekly you pull the strings and send them off, then they come back a week or more later, you check that data in and it's available after your next deployment.

Just FYI, I encountered a "translation console" recently via the Directus project - they use a system called Crowd-In. Directus is using vue-i18n and .yaml files for translations, but Crowd-In supports many formats.

@dominikg
Copy link
Member

I think using separate translation keys instead of the primary language string is both more stable and flexible.

It allows for namespacing and translation of generated keys, and should make it easier to implement tooling around extraction / missing translations checks etc.

I also think providing context for translators should not be done in the template part of a .svelte component as that would be very verbose and detrimental to quickly understanding a components structure.

Svelte is in a kind of unique position here as with svelte-preprocess, the compiler, language-tools and sveltekit we have all the tools at hand to build a system that allows to use a wide array of possible input formats, ( eg. an extra <translations></translations> element in .svelte components consumed by a markup preprocessor, trees imported from json or yaml, .po,... to different output formats based on the applications needs (static precompiled, static + language-store bundle for switching, fully clientside dynamic/runtime) and provide developers and translators the information to efficiently do their jobs.

@floratmin
Copy link

floratmin commented May 14, 2021

@johnnysprinkles
I think that in many use cases the language is switched mostly only one time. This holds also for my use case. So I developed this part first. But I am thinking to implement two other use cases. The first would be to have per page a store with all translation strings of all languages so that switching to any other language is immediate. The second would be to serialize the functions and fetch them on demand. This would give the possibility to store fetched translation strings. For these two solutions, you have to keep in mind, that a lot of language-specific cardinals could be included in the package.

The functions for Page.svelte from the second example in my previous comment could e.g. be like
// in Page.svelte (only one version)
<script lang="ts">
    import { t } from './t/t2';
    export let d: Date;
    export let res: number;
</script>
// later
<p>
    {$t.a7o({D: d})}
</p>
<p>
    {$t.a7n({RES: res})}
</p>
// t2.js
import { writable, derived } from 'svelte/store';
import { plural } from "@messageformat/runtime";
import { date } from "@messageformat/runtime/lib/formatters";
import { en, de } from "@messageformat/runtime/lib/cardinals";

export const language = writable('en'); // set initial language

const fallbacks = {
    de: 'en'
};
// we can merge more functions from shared modules here
const _0 = {
    plural,
    date,
    de,
    en,
};
// we could also import shared message functions and merge them here
const languageStrings = {
    'en': {
        a7n: m => 
            (
                (d) => _0.plural(d.RES, 0, _0.en , { "0": "No bar", one: "One bar", other: "Some bar" })
            )(m),
        a7o: m => ((d) => _0.date(d.D, "en"))(m)
    },
    'de': {
        a7n: m => 
            (
                (d) => _0.plural(d.RES, 0, _0.de , { "0": "Kein bar", one: "Ein bar", other: "Ein paar bar" })
            )(m),
        a7o: m => ((d) => _0.date(d.D, "de"))(m)
    }
};
// we could also implement some fetch functionality and deserialization here 
// some fallback for missing strings can also be implemented here
export const t = derived(language, ($a, set) => {
    set(languageStrings[$a] ? languageStrings[$a] : languageStrings[fallbacks[$a]])
});

Some thoughts regarding your list of features:

  • Inline strings: Yes
  • Labeled string: This could be implemented as a special comment
  • Route limited: Yes (when using javascript modules, imports and strings could be shared)
  • extract-i18n/apply-i18n: Implementation should be straight forward
  • Client-side language change: Will be implemented (see example above)
  • Hashed keys: If text or text and context changes, the gettext workflow triggers a re-translation
  • Three-stage pipeline: I am not sure if translators are better served with a refined text or with raw text and comments. If this should be necessary there could be a step in between where the refined .po file could serve as the .pot file for creating the .po files for the translators. A simple JSON file could map strings between raw and refined text.
  • Fallback to history: There is a feature in gettext #| msgid previous-untranslated-string and #| msgctxt previous-context which is not well supported in most implementations and editors. This could allow a fallback to the previous version and could also provide previous translations of the message string for translators. I wanted to include this functionality, but gettext-extractor depends on pofile and there this feature is not implemented. A problem with this approach is that we have to mark a message string explicit to be a new version of some older message string. This puts more work on the programmer. Also it makes the code more verbose. Some of the connections to a previous version could get lost if strings are changed more than one time per translation cycle.
  • Fallback, manual: There is the possibility to use compendia in gettext. New messagestrings get looked up and pre-populated. There is also some mechanism called fuzzy entry in gettext which matches strings with little differences, but I did not look into it.
  • IDE support: Except when checking for correct types when using with typescript, there is no IDE support.

@floratmin
Copy link

@dominikg I think using the primary language string (and some context if necessary) to be the better option. When a project is fresh and small, creating a hierarchy of keys is easy. But after some refactorings or some new developers on the project, the maintenance of this hierarchy gets quickly more and more hard. If I use only the string it is more like set it and forget it. If I have to differentiate, I simply add some context. See also this comment and the entire discussion on StackOverflow. I think also that retranslating one string is often cheaper than creating and maintaining a system for avoiding retranslation.

I see also that providing the context/comments directly in the source code not to be a big problem. Most strings in applications are trivial and don't need a lot of context or comments. And keeping context/comments separated in another part of the file makes the maintenance harder.

@mabujaber
Copy link

Do you put Right to left in consideration or you consider it as a different feature?

@floratmin
Copy link

@mabujaber Right to left can be almost completely solved with CSS. You have only to set dir on the body element to rtl or ltr with svelte to match the direction of the language. Then you can use body[dir="rtl"] and body[dir="ltr"] as selectors. If you use tailwind.css, have a look at tailwindcss-rtl.

@blindfish3
Copy link

@samuelstroschein - I take your point; and I accept I'm looking at this from a particular (in my experience typical) workflow: designs are provided with text embedded and devs are expected to work from there. Pre-keyed translations are not available; and sometimes designs aren't stable and subject to change. With the manually keyed solution a dev might have to come up with an id and add it to the source file and land up having to manage that file later. What I'm proposing is that the dev just copy pastes the initial text and (almost) never changes it. A parsing process uses this initial text to generate an id and this is used in the source translation files; which is generated automatically. I totally agree that the ability to change the 'native' language text without requiring dev intervention is really useful; but that can also be achieved simply by having a translation file for the native language (and this could even be rendered into the output during the preprocess step). Also the parsing approach doesn't preclude using 'keys' in the template instead of text.

@inta - different syntax is the norm in Angular and IMO it worked fine there. I'd also prefer to avoid use:i18n and simply have i18n as in Angular; and ideally have this built into Sveltekit so it can be used in component tags. It obviously depends on the application; but in most cases this was all I ever needed to add for translation in templates. But the tagged template approach would be a good compromise if that can work with variables.

@samuelstroschein
Copy link
Contributor

@blindfish3

What I'm proposing is that the dev just copy pastes the initial text and (almost) never changes it. A parsing process uses this initial text to generate an id and this is used in the source translation files; which is generated automatically.

Con you confirm that the feature request I just opened here opral/monorepo#111 is what you mean?

Pre-keyed translations are not available; and sometimes designs aren't stable and subject to change.

That seems like a hand-off gap. Theoretical question: If designers create the text in their design files, why is copy & pasting required from developers? A Figma/Sketch/etc. plugin could eliminate the hand-off.

@blindfish3
Copy link

A Figma/Sketch/etc. plugin could eliminate the hand-off.

TBH I'm not aware of these features and nor were the designers I was working with previously 😅
But I think we're straying from the topic: the point is: Sveltekit needs a generic solution that can support different workflows and scale well. To some extent I like the idea of using a preprocessor since that's an easy way to make the supported solution optional; and it avoids the need to manage (or manually create) the source file. The suggestion for use:i18n was just a way to eliminate all the brackets you have to type if you're using {$t('my text or id')}. If that approach were to be considered then what would be nicer would be if Sveltekit exposed an i18n attribute and left the implementation details to developers...

@Rich-Harris Rich-Harris added this to the post-1.0 milestone Apr 25, 2022
@Gin-Quin
Copy link

Gin-Quin commented May 12, 2022

I'm adding my piece to this big topic. Since a lot has already been said, I'll be as straightforward as possible.

String keys VS objects

String keys are most of the time a bad pattern. It's hard to type and you can't have nested values. You just have one big flat namespace.

Regular objects are better for autocompletion and complex typing so I would go with that.

I've read in this topic than some cool plugins integrate with i18n string keys and show previews. That's super cool, but it can be done as well with regular objects - even so it may not exist yet.

Global translation files VS in-component translation

I think integrating translation at component-level rather than at global level is an easy way to make sure to not import all translations at once. You import a component => it comes with its own translation. Simple and effective.

Also Svelte has an approach of single-component, ie mixing different languages in the same file to create a reusable component. It makes sense to follow this guideline and integrate not only script (JS), template (HTML) and style (CSS), but also a content (ICU / Fluent) part inside the component.

The drawback of having an in-component translation is: how to integrate it with translation softwares? It must be easy to parse to allow a svelte preprocessor to only "extract" the translation part of the component.

Example of svelte file with an added "content" section:

<script>
  .. my logic
</script>

.. my template

<style>
  .. my style
</style>

<content lang="fluent | yaml | toml | json">
  .. my translations
</content>

I prefer the naming "content" over "translation" because a lot of stuff can depend on the current language, not only text but also images, links, ...

Also the script should be placed at the bottom of the file because it can get very large when manipulating a lot of languages - and if the user works with a 3rd party translation software, this part can even be edited automatically.

Preprocessing content

The <content> script would be preprocessed into JS and create a $content Svelte readable store accessible in <script> and <template>.

This is quite the easy part. I created a very small library that already does something similar: https://www.npmjs.com/package/@digitak/cox

Type safety

With that architecture, it's easy to check if keys are missing or not. It's the role of the preprocessor.

No JSON (or not only JSON)

JSON is not a language made for humans. It's great for computers. I would prefer a language like YAML, TOML or Fluent.

Concrete example with YAML using ICU strings:

<p>
  {$content.greetings}
</p>
<p>
  {$content.nested.easy}
</p>

<content lang="yaml">
  en:
    greetings: "Hello world!"
    nested:
      cool: "That's easy, right?"
  fr:
    greetings: "Salut le monde !"
    nested:
      cool: "C'est facile, n'est-ce pas ?"
</content>

Using arrays

Sometimes you need to manipulate arrays of different lengths depending on the language. For example, you want to show some customer reviews in your landing page, but you have only 2 reviews in Polish when you have 5 in english.

That would be great to support arrays as well.

@cibernox
Copy link

cibernox commented May 12, 2022

This is only marginally related to the topic at hand, but in svelte-intl-precompile (https://svelte-intl-precompile.com/) I've somewhat recently added support for more formats other than JSON, particularly JSON5 (yay comments!) and yaml. I don't use yaml but JSON5 improves regular json ergonomics a lot.

I also added support for number skeletons so it allows complex number formatting like {progress, number, ::percent scale/100 .##} completed or Your account balance is {n, number, ::currency/CAD sign-always}.

@geniuskrul
Copy link

None of this nonsense, please:

Screenshot1

Screenshot2

Screenshot3


Everyone knows Wordpress as an old tool that doesn't do a lot of things right, but here I see a lot of people talking about using JSON files for translations😐🔫.

The current way of Wordpress doing translations is via using .pot, .po, and .mo files. Inside a .php file, you wrap your text inside a function like this:

__('Hello World')

...and that's it. You don't need to create any file to keep your translations in, don't need to create a name for each string inside the JSON file, no need for nonsense extra work. You can use tools like PoEdit software, Loco Translate and Automatic Translate Addon For Loco Translate plugins. You can automate translations with Google Translate API (or other APIs). You can use currently added translation to help you speed up the translation process. You can do a lot of other great things that you can find it all in the links above.

@ivanhofer
Copy link
Contributor

The file format doesn't really matter. It's just some implementation detail from the software you are using.

The important thing is, that there exist tooling around those file formats. Ideally with strong support for tranlation-specific formats like variables, plural rules and formatting. Not just simle text-fields.

Automatic extraction of strings from files can be a great thing but it also needs some context information about where it is beeing used. Similar to the nesting structure of e.g. JSON translations: video.controls.play gives a translator more clues where it's beeing used than a simple string with 'play'. Seeng the context the german translation would be 'Wiedergabe'. Without the context this would probably become 'spielen'

@geniuskrul
Copy link

The file format doesn't really matter. It's just some implementation detail from the software you are using.

It does matter because software and a lot of other apps are using the same, there is also a big community and resources behind it.

Automatic extraction of strings from files can be a great thing but it also needs some context information about where it is beeing used

There are already multiple solutions available for kinds of things. The solution that I explained above, it's a mature feature. Such simple issues are a piece of cake. By using JSON, you make the experience hell and miserable both for the developers and translators. There is not even a single benefit in using JSON or similar formats and solutions. Instead of sticking to what you know, try other solutions. No one wants a lazy feature like using JSON files for translations.

@samuelstroschein
Copy link
Contributor

It does matter because software and a lot of other apps are using the same, there is also a big community and resources behind it.

THE file format for translations does not exist. Every programming language, framework, and even SDK uses its own implementations. .po for PHP apparently, localizable strings for iOS, XML for Android, JSON for some, etc.

That said, I do believe there should be one file and syntax format for translations. Mozilla's Fluent project seems to be the most promising one https://projectfluent.org/, and is the one we are building https://github.com/inlang/inlang on top of.

@ivanhofer
Copy link
Contributor

It does matter because software and a lot of other apps are using the same, there is also a big community and resources behind it.

In the end every file format and message syntax could be transformed to another format. Translators probably don't want to write .po files. What the really want is having a nice UI to just write text snippets and don't care about technical aspects of a translation processs. Some automated tooling then transforms translations into files that can be used by an i18n library.

No one wants a lazy feature like using JSON files for translations.

I personally also don't like using the JSON file format for storing translations. There exist better solutions that offer more context
like .po files or better typesafety like with .ts files.

@benmccann benmccann added the size:large significant feature with tricky design questions and multi-day implementation label Aug 1, 2022
@nuclear-icebreaker
Copy link

nuclear-icebreaker commented Aug 22, 2022

For two hours read this discussion, but did not see main benefit of using __('Base language fallback text') syntax or maybe <p use:i18n>Base language fallback text</p> (and then .po, . pot etc.) mentioned. Main benefit of these approaches is almost none overhead of building working and understandable application in developer's native language and still leaving (quite easy and straight forward) possibility of translating application in distant or not so distant future when application code becomes somewhat stable.

This approach does even leave possibility of application not being translated and developers time not wasted on making .json files or thinking up unnecessary unique or reusable keys - just text, that needs to be where it needs to be.

Edit: As one man band I can do any of the translation workflows. All of my projects have multi-language requirement (living in small country where third of population speaks another language). But in the end 4 out of 5 of applications I write are left untranslated. So you can see possible frustration of making a lot of unnecessary moves on my side to make translation even possible.

I do see benefits of keyed translations like $t(home.title) for projects that are text heavy and will be translated no matter what, where developer is responsible for only functionality, not looks or content. That is understandable, but, please, do not forget about us small guys, who make everything alone...

PS I fell in love with SVELTE a week ago. Loving every bit of it! Maybe direction of i18n could be better...

Keep up the good work!

@idleman
Copy link

idleman commented Oct 27, 2022

Don´t force local translation files please.

Translation files in every project I have seen have always been a mistake in the long term. The larger the website/application grows, the more rapidly will translations change and it is not desirable re-build the app every time when it happens. You deeply want to fetch that information from a database or external service in the long term.

A solution would be to let the user specify a "fetch" function. A default implementation could read some local file, but it would allow the developer to fetch from a database or external SaaS service - if it need too. Best of both worlds.

But of course, some "typed" information may get lost with the above solution, but it is worth to lose it 100% if it give the flexibility to fetch translation however the developer wishes.

@stalkerg
Copy link
Contributor

Just in case, I currently use https://github.com/cibernox/svelte-intl-precompile and it's working very well, for the primary key, I am using an English default value.
If you have >500 strings, it's challenging to prepare a good hierarchy and short names for all strings.

@cibernox
Copy link

@stalkerg thanks for the shout out. I'm following this thread in case there's anything I can do to improve the library.

@ljani
Copy link

ljani commented Dec 30, 2022

I'm using ttag with this patch to support TypeScript and this as a loader for .po files with Vite and finally PoEdit for doing translations. As ttag produces .po files similar to GNU's gettext, other software could be used as well.

Because ttag is a third-party library, it doesn't implement Svelte's store interface. I've wrapped the whole application in a {#key $locale} block in addition to invalidateAll call to invalidate translated titles from load when changing the language.

I think the setup is pretty good and I really like using tagged template literals for translations. Hope my PR will get merged making using TypeScript easier. Here's an example:

+layout.svelte:

<script lang="ts">
    import { currentLocale } from "$lib/i18n.js";
</script>

{#key $currentLocale}
    <main>
        <slot />
    </main>
{/key}

+page.svelte:

<script lang="ts">
    import { t } from "ttag";

    const text = t`The setup is pretty good.`;
</script>

<h1>{t`Hello world!`}</h1>
<div>{text}</div>

@samuelstroschein
Copy link
Contributor

We are exploring building a metaframework agnostic i18n library that would solve this issue.

Discussion is ongoing in https://github.com/orgs/inlang/discussions/395

@ivanhofer
Copy link
Contributor

A small update regarding the metaframework agnostic i18n solution mentioned above:

We will soon start with the implementation. The first framework that we will support is SvelteKit 🎉.
I have written an RFC for the first phase: opral/monorepo#460
It would be great if you could give us some feedback.
Thanks!

@Myrmod
Copy link

Myrmod commented May 4, 2023

That's how I currently am handling translations in SvelteKit.

  1. I need a custom store:
import { browser } from '$app/environment'
import { getCookie, setCookie } from 'helper-functions/src/cookies' // simply wrapped calls to document.cookie
import { get, writable } from 'svelte/store'
import translations from './translations'

export const locale = writable(browser ? getCookie('locale') : 'de' || 'de')
export const locales = Object.keys(translations) as unknown as keyof typeof translations

function translation() {
	const { subscribe, set } = writable(translations[get(locale)])

	return {
		subscribe,
		changeLanguage: (language: keyof typeof translations) => {
			if (!translations[language]) {
				console.error(`the language "${language}" does not exist`)
				return
			}
			set(translations[language])
			locale.set(language)

			if (browser) setCookie('locale', language)
		},
		locale,
		locales,
	}
}

export const t = translation()
  1. I need translation files to begin with. I am using JS objects, so that I can have proper type checking, for missing keys.
import type de from './de' // this is a simple exported JS object, which provides keys and acts as default language. It could also be used as a fallback

const en: typeof de = {
	title: 'Hello SvelteKit',
}

export default en
  1. Using the store as follows:
<script lang="ts">
	import { t } from '$lib/stores/language'
</script>

<h1>{$t.title}}</h1>
  1. I need to be able to set the correct language on load, for this I use a +layout.server.ts file
import { locale, locales, t } from '$lib/stores/language.js
import { get } from 'svelte/store'

/** @type {import('./$types').LayoutServerLoad} */
export function load({ cookies }) {
	const supposedCurrentLocale = (cookies.get('locale') as typeof locales) || 'de'

	if (supposedCurrentLocale !== get(locale)) {  // better be sure to not set it twice, maybe not needed here
		t.changeLanguage(cookies.get(supposedCurrentLocale) as typeof locales)
	}

	return {}
}
  1. for translating routes, the only solution I have come up with is, to create a custom page in the translated route that you need and import the "base" . It's definitely not perfect, especially if you handle a lot of data, but it works.
<script lang="ts">
	import Page from 'path/to/original/page/+page.svelte'

	export let data: any
</script>

<Page {data} />

Maybe for the route handling we could implement a file like +aliases.ts, whcih could look something like this

const aliases = [
  '/my/translated/path/1',
  '/my/translated/path/2',  
]
export aliases

This file could be used to generate the above mentioned routes automatically. This way the solution I'm currently using would be great for me.

best regards

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature / enhancement New feature or request p1-important SvelteKit cannot be used by a large number of people, basic functionality is missing, etc. size:large significant feature with tricky design questions and multi-day implementation
Projects
None yet
Development

No branches or pull requests