opral / inlang-paraglide-js

Tree-shakable i18n library build on the inlang ecosystem.
https://inlang.com/m/gerre34r/library-inlang-paraglideJs
50 stars 1 forks source link

Computing keys from compile-time constants #164

Open nosovk opened 4 months ago

nosovk commented 4 months ago

I want to start using paraglide in sveltekit, but I have a bit strange use case.

My translation file:

{
  "promotion": "super Promotion",
  "title": "super Title",
  "variation": {
    "promotion": "Promotion variation",
    "title": "Title variation",
  },
  "custom_client": {
    "promotion": "client promotion",
    "title": "client Title",
  }
}

Then in my component: component.svelte

<script>
import { getContext } from 'svelte';
import { t} from 'paraglide';
const i18nPrefix = getContext('settings').i18nPrefix ?? "";
</script>
<h1>{t(i18nPrefix+"promotion")}</h1> <!-- it will produce "custom_client.promotion" key -->
<p>{t(i18nPrefix+"title")}</p>

inside router in svelte I create files like: /src/routes/custom_client/+page.svelte

<script>
import Component from "$lib/Component.svelte";
setContext<LandSettings>("settings", {i18nPrefix: "custom_client." })
</script>
<Component />

In case above we use some prefix constant, that modifies scope of translations, inside some route. Actually we have one landing, but we have dozens of variations with different promotions. Currently we use selfmade translation service, but we have to send full {lang}.json to clients, with all variations. Currently its size is pretty big, and we tried to move to paraglide, because treeshakable translations is what we need.

I've found that there are two different approaches in paraglide - t and m plugins, but wasn't able to make composable key in none of them. Also I found mention that you're not going to support nested messages, that we highly dependant from (https://github.com/opral/inlang-paraglide-js/issues/159). But I see that you use some other message formats, that support nested messages. Probably we can achieve the same with custom source plugin?

nosovk commented 4 months ago

It seems that i18next namespaces should cover that case

LorisSigrist commented 4 months ago

Hi đŸ™‹

The usage like you outlined unfortunately can't work. Any form of dynamic key, like m[prefix + ".promotion"], on the client will break treeshaking. Bundlers won't be able to determine the possible values at build-time.

This would cause all messages to be loaded, regardless of if they are used.

If you want to only load the messages for the current i18nContext you need to render the messages on the server & pass them to the client.

// src/routes/+page.server.ts
import * as m from "$lib/paraglide/messages"

export const load({ locals }) {
  return {
    promotion: m[locals.i18nContext + "_promotion"](),
    title: m[locals.i18nContext + "_title"](),
  }
}
<script>
  export let data;
</script>

<h1>{data.title}</h1>
<p>{data.promotion}</p>

As for nesting: Paraglide does not support nesting, even if a plugin that does support it is used to load the messages.

We recommend to do one of the following:

nosovk commented 4 months ago

Hm, but there is no dynamic key. The key is actually static. Like https://webpack.js.org/plugins/define-plugin/ long time before. And it's actually processed by vite as static string. In my example we use context, which is actually rendered on ssr for example.

I mean that there is no need to make dynamic key load, it should be statically parametrized :)

I was thinking about that namespace thing from https://inlang.com/m/3i8bor92/plugin-inlang-i18next Could it be used to add prefix to all language strings in a context scope?

nosovk commented 4 months ago

Ok, after bit more testing I now understand that even if I use i18next source files with nesting, there is no way to use it in paraglide, because its converted to simple kv. And : which is used to create divider does not supported as literal in function names, which causes paraglide to fail.

LorisSigrist commented 4 months ago

I just tested the keys with static parameterization & vite does not statically parameterize the keys, even if they are computed only from build-time constants.

It seems that any sort of computation for the key used to index into a namespace will break treeshaking in the current generation of bundlers. Only hard-coded constants work.

LorisSigrist commented 4 months ago

You're not the only one with this requirement. A while back Eric Burel from the State of JS/HTML survey approached us with a similar use-case. They needed to use the year + the topic of the survey as a namespace, similar to what you're doing here.

Clearly this is something people expect to work. We should open a feature request in Rollup

nosovk commented 4 months ago

https://vitejs.dev/config/build-options#build-minify vite supports terser, which is does those modifications.

image

it seems that there is a reason to use it for prodcutin builds

mrIliya commented 3 months ago

Yep, we also need that feature, would be nice to have it

Kowalski0805 commented 2 months ago

+1, we are using sveltekit, and we're investigating an opportunity of using paraglide as i18n backbone in our application. Unfortunately, this feature is a dealbreaker in comparison with https://github.com/kaisermann/svelte-i18n

LorisSigrist commented 2 months ago

This is absolutely something we want to see, but it's fundamentally a bundler limitation. As bundlers get better this will automatically start to work with no change on paraglide's side.

I did try the following approaches but could not get vite to treeshake reliably:

1. Vite + Terser + constant

Using terser unfortunately doesn't solve this today. The following code doesn't get treeshaken properly.

import * as m from "../paraglide/messages.js"
const year = "2024"
console.log(m[`msg_${year}`])

This is also unlikely to work soon. Consider the situation where the year is imported from another file. In this case you would need to resolve it from a different file before you can optimize this one. You would have to do the bundling step twice. This would require major changes to how our bundlers currently work.

2. Using define to replace values

In vite.config.js you can provide a define option which allows you to replace certain values at build time.

define: {
   __YEAR__: "'2024"
}

Unfortunately this doesn't work either:

import * as m from "../paraglide/messages.js"
console.log(m[`msg_${__YEAR__}`])

However, this approach is the most likely to work soon, since there aren't really any major changes needed to the bundler internals. I'll open some issues.

Update: I've tried the define approach with just EsBuild (instead of vite) & that seems to work. If rollup were to support the same define feature then this would be the way to go

LorisSigrist commented 2 months ago

I've opened an issue in vite about this: https://github.com/vitejs/vite/issues/17898

kocetora commented 2 months ago

wow, I've spent 3 hours trying to do that, because its certainly unclear from docs that its not working. There is https://inlang.com/m/3i8bor92/plugin-inlang-i18next in a docs, which clearly supports namespaces, but its actually not working with paraglide :(

foobar11101011 commented 2 months ago

We're using paraglide at the current project, and can't move out. But we also stuck with that problem, when we tried to implement user customizations to bundles. I see some workarounds in Vite, but it does not look like it works with paraglide.

NataliiaSe commented 2 weeks ago

Any updates on it? We are now evaluating different options

samuelstroschein commented 2 weeks ago

@nosovk @NataliiaSe Do I understand the requirement correctly?