cibernox / svelte-intl-precompile

I18n library for Svelte.js that analyzes your keys at build time for max performance and minimal footprint
https://svelte-intl-precompile.com
ISC License
274 stars 13 forks source link

feat: auto register all available locales #32

Closed sastan closed 2 years ago

sastan commented 2 years ago

The prefix ($locales) is exposed as a module and can be used to auto register all available translations.

import { init, waitLocale } from '$locales':

init({ initialLocale: en });

export async function preload() {
  return waitLocale(); // awaits the default locale, "en" in this case.
}

The generated module has a default export to retrieve a list of all available locales. This can be used to select an available language.

import registeredLocales, { getPossibleLocales } from '$locales'

// acceptedLanguages could have been parsed 'accept-language' header or navigator.languages
function detectPreferredLocale(acceptedLanguages) {
  // first try the exact matches
  for (const language of acceptedLanguages) {
    if (registeredLocales.includes(language)) {
      return language
    }
  }

  // then try possible locales
  for (const language of acceptedLanguages) {
    for (const locale of getPossibleLocales(language)) {
      if (registeredLocales.includes(locale)) {
        return locale
      }
    }
  }

  // fallback
  return 'en'
}

In the future I would like to use module augmentation to generate typescript types for $locales. This would allow type checked access to $format.


For the following files:

<localsRoot>/de.json
<localsRoot>/en.json
<localsRoot>/es.json

The $locales module would look like:

import { get } from 'svelte/store'
import { register, locales } from 'precompile-intl-runtime'

export * from 'precompile-intl-runtime'

register("de", () => import('$locales/de.js'))
register("en", () => import('$locales/en.js'))
register("es", () => import('$locales/es.js'))

export default /* @__PURE__ */ get(locales)
sastan commented 2 years ago

I just realized I didn't update the readme. If you would like to merge this I would update the readme in this PR before merging.

cibernox commented 2 years ago

I think it's a great idea to expose a way of automatically registering all possible locales. I'm not so sure about making $locales reexport everything from precompile-intl-runtime. I'd rather make $locales only export two things: The list of available locales and an async function to register loaders for all locales:

Example usage:

import { registerAll, availableLocales } from '$locales';

registerAll(); // Equivalent a bunch of `register("lang", () => import('$locales/lang.js'))`

init({ initialLocale: findBestLocale(availableLocales) });

All utilities from precompile-intl-runtime are already reexported from svelte-intl-precompile, and I'd rather not have the same stuff exported from two different paths.

sastan commented 2 years ago

Sorry this got quite long...

I didn't like to use a registerAll method because if you do not call it there are no registered locales. But the whole idea of the module is auto registering the locales. For me that is the intended side-effect when importing the module. An additional positive aspect would be that they are registered only once. But registerAll could run several times.

Is there a use case where you wouldn't want to register all?

But if you want $locales to be side-effect free without running into the issue that availableLocales is empty maybe we could have registerAll return the available languages:

import { registerAll } from '$locales';

// work in svelte and hooks
const registeredLocales  = registerAll(); // Equivalent a bunch of `register("lang", () => import('$locales/lang.js'))`

init({ initialLocale: findBestLocale(registeredLocales) });

I really like the re-exports because that means my source file are having one import less. And I'm working on generating typescript types for locales. That would allow to type check $format() calls. The additional module ($locales) would allow to add different types to format.

What do you think about limiting the re-exports? Maybe we only exports format, _, t, date, number, time, locale, locales, ???


My use case is within sveltekit. First with registerAll and no re-exports, then how I use it currently :

routes/__layout.svelte:

<script context="module">
  import { init, waitLocale } from 'precompile-intl-runtime'
  import { registerAll } from '$locales'

  // I personally do not like this lone call
  registerAll()

  export function load({ session ) {
    init(session.intl)
    return waitLocale()
  }
</script>

routes/index.svelte:

<script>
  import { t } from 'precompile-intl-runtime'
</script>

<h1>{$t('title')}</h1>

and in hooks.js:

import { getPossibleLocales } from 'precompile-intl-runtime'
import { registerAll, availableLocales } from '$locales'

// I personally do not like this lone call
registerAll()

export function getSession(request) {
  return {
    intl: {
       initialLocale: detectPreferredLocale(parse(request.headers['accept-language'])),
       fallbackLocale: 'en',
     } 
  }
}

function detectPreferredLocale(acceptedLanguages) {
  for (const language of acceptedLanguages) {
    if (availableLocales.includes(language)) {
      return language
    }
  }

  for (const language of acceptedLanguages) {
    for (const locale of getPossibleLocales(language)) {
      if (availableLocales.includes(locale)) {
        return locale
      }
    }
  }

  return 'en'
}

This is how I like to use it:

routes/__layout.svelte:

<script context="module">
  // just one import - no need to import precompile intl as well and all locales are already registered
  import { waitInit } from '$locales'

  export function load({ session ) {
    return waitInit(session.intl)
    // function waitInit(options) {
    //   init(session.intl)
    //   return waitLocale()
    // }
  }
</script>

routes/index.svelte:

<script>
  // convenience re-export - in the future this can be typed by analyzing the generated code from the json files
  import { t } from '$locales'
</script>

<h1>{$t('title')}</h1>

and in hooks.js:

import registeredLocales, { getPossibleLocales } from '$locales'
// or maybe
import { registeredLocales , getPossibleLocales } from '$locales'

export function getSession(request) {
  return {
    intl: {
       initialLocale: detectPreferredLocale(parse(request.headers['accept-language'])),
       fallbackLocale: 'en',
     } 
  }
}

function detectPreferredLocale(acceptedLanguages) {
  for (const language of acceptedLanguages) {
    if (registeredLocales.includes(language)) {
      return language
    }
  }

  for (const language of acceptedLanguages) {
    for (const locale of getPossibleLocales(language)) {
      if (registeredLocales.includes(locale)) {
        return locale
      }
    }
  }

  return 'en'
}

I'm open to all suggestions and your preferred solution and will adjust this PR accordingly.

Thanks you for considering this.

cibernox commented 2 years ago
  1. Automatically register locales without a function call. It's more of a philosophical stance. I'm not a fan of modules that run side effects on import. It also prevents tree shaking of code within that module if someone only wants to check the available locales. I'd rather export a function that when called registers the locales. Calling it several times doesn't seem too concerning, because I don't think it will happen in practice and secondly because registering the same locale twice does no harm.

  2. Is there any reason not to register every available locale? I don't have one, but maybe. Maybe some locale is in beta and developers don't want to register (which would make it selectable) if half the translations are still missing. I myself would rather using an app 100% in english that one in which half the text is in english and half in spanish. Or maybe they want to do some A/B testing of the new locale. It's hard to anticipate what people will do.

  3. I'd rather still make people import utilities from svelte-intl-precompile and not from $locales. There are two reasons. First, the $locales import path is optional, if you define your translations in JS files instead of json don't use it for your locales. Second is that having an API that is nearly 100% compatible with svelte-i18n is very valuable. Right now someone with an app using svelte-i18n can go change to this package, do a find-and-replace of svelte-18n into svelte-intl-precompile and hook the compiler in svelte-intl-precompile and they are good to go. They will feel very familiar. Importing from $locales is a bit (a lot) magical and might throw some people off.

sastan commented 2 years ago

Alright. Got you. I'll adjust the PR.

Just two last questions.

  1. I personally would prefer to not have a availableLocales export because that might be empty if registerAll has not been called.

      import { registerAll } from '$locales';
    
      const registeredLocales  = registerAll(); // Equivalent a bunch of `register("lang", () => import('$locales/lang.js'))`

    or

      import { registerAll, availableLocales } from '$locales';
  2. What should availableLocales include? Only the locales that have been registered within $locales? Or all locales (basically get(locales)) which may include locales not registered by $locales?

cibernox commented 2 years ago

If an user wants to know the registered locales they can already today do it checking the $locales store. I vote that availableLocales should be all the locales you can register, even if you haven't registered them yet. Probably you can export a simple array that you generate in a similar way to how you generate the import statements:

`export const availableLocales = [{fs.readdirSync(localesRoot).map(file => "'${file}'").join(',')}];`
sastan commented 2 years ago

😄 Jep – just came to the same conclusion.

sastan commented 2 years ago

Done. Here is an example output:

import { register } from 'svelte-intl-precompile'
export function registerAll() {
    register("de", () => import('$locales/de.js'))
    register("en", () => import('$locales/en.js'))
}
export const availableLocales = ["de","en"]
cibernox commented 2 years ago

That's perfect. Please, add an entry to the Changelog and I'll merge it and release a new version tonight.

cibernox commented 2 years ago

Thanks!

cibernox commented 2 years ago

Published in 0.7.0 🎉

EskelCz commented 2 years ago

What is the recommended way of auto-registering translations in .js files? The $locales is not working for me in that case. I'd rather not explicitly name all of them.

cibernox commented 2 years ago

@EskelCz there'a also a import { availableLocales } from '$locales'; that you could use to add them in a loop if you load them asynchronously. I do have some questions tho:

  1. What's not working of the $locales import path?
  2. Why .js files and not json files?
EskelCz commented 2 years ago

@cibernox

  1. Sorry seems like the $locales is not working in general, even the static imports. I'm using the latest sveltekit, basically the skeleton project, the path is /src/locales/, but I guess I missed something.
  2. I like being able to use code. My second choice would be json5.
cibernox commented 2 years ago

@EskelCz I suspect that you've configured your locales to be in /locales and instead you've placed them in src/locales, but that's just a hunch. But $locales should definitively work, and if it doesn't it would be very bad and it would be my top priority to fix it. If that doesn't fix it maybe you can provide some simple reproduction in https://codesandbox.io/?

EskelCz commented 2 years ago

@cibernox That was exactly it, sorry. Thanks a lot for such a quick response :) registerAll doesn't seem to work for js files though, gonna try the import loop then.

EskelCz commented 2 years ago

Seems like the issue isn't js files after all. Tried json and still the autoregister doesn't seem to work. Getting [svelte-intl-precompile] The message "topics" was not found in "".

/src/routes/_layout.svelte

  import { init, waitLocale, register } from 'svelte-intl-precompile'
  import { registerAll, availableLocales } from '$locales'
  registerAll()
  export async function load() {
    init({ initialLocale: 'en', fallbackLocale: 'en' })
    await waitLocale()
    return {}
  }

/locales/en.json

{
  "topics": "Topics"
}

/src/routes/index.svelte

<script>import { t } from 'svelte-intl-precompile'</script>
<h1>{$t('topics')}</h1>

/svelte.config.js

import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
const config = {
  extends: './.svelte-kit/tsconfig.json',
  kit: {
    vite: {
      plugins: [
        precompileIntl('locales')
      ]
    }
  }
}
export default config
EskelCz commented 2 years ago

It worked in a module, like this:

<script context='module'>
  import { init, waitLocale, register } from 'svelte-intl-precompile'
  import { registerAll, availableLocales } from '$locales'
  registerAll()
  export async function load() {
    init({ initialLocale: 'en', fallbackLocale: 'en' })
    await waitLocale()
    return {}
  }
</script>

Even the js files :)

EskelCz commented 2 years ago

I followed this example, so I guess it's outdated: https://svelte-intl-precompile.com/en/docs/configuration#dynamic-locales

cibernox commented 2 years ago

@EskelCz what's outdated from that page? The load function is a sveltekit function that has to be returned from a <script context="module">, but that's just how sveltekit works. I should probably be more specific in the code snippet.

EskelCz commented 2 years ago

@cibernox Sorry you're right, I'm pretty new to Sveltekit so that clarification would help me a lot. Anyway thanks for a great library, now it's working flawlessly.