Git Product home page Git Product logo

ivanhofer / typesafe-i18n Goto Github PK

View Code? Open in Web Editor NEW
2.1K 9.0 74.0 24.25 MB

A fully type-safe and lightweight internationalization library for all your TypeScript and JavaScript projects.

Home Page: https://github.com/ivanhofer/typesafe-i18n

License: MIT License

TypeScript 100.00%
typescript internationalization localization i18n javascript angular lightweight react svelte angular2

typesafe-i18n's Introduction

๐ŸŒ typesafe-i18n

A fully type-safe and lightweight internationalization library for all your TypeScript and JavaScript projects.

npm version GitHub Top Language bundle size types included bump version & publish to npm Generic badge Sponsor this project

Advantages

๐Ÿค lightweight (~1kb)
๐Ÿ‘Œ easy to use syntax
๐Ÿƒ fast and efficient
๐Ÿฆบ prevents you from making mistakes (also in plain JavaScript projects)
๐Ÿ‘ท creates boilerplate code for you
๐Ÿ’ฌ supports plural rules
๐Ÿ“… allows formatting of values e.g. locale-dependent date or number formats
โ†”๏ธ supports switch-case statements e.g. for gender-specific output
โฌ‡๏ธ option for asynchronous loading of locales
๐Ÿ“š supports multiple namespaces
โฑ๏ธ supports SSR (Server-Side Rendering)
๐Ÿค can be used for frontend, backend and API projects
๐Ÿ” locale-detection for browser and server environments
๐Ÿ”„ import and export translations from/to files or services
โ›” no external dependencies

Interactive Live Demo

Click here to see an interactive demo of typesafe-i18n showing some key aspects of the type-checking capabilities of this internationalization library.

Works with

Table of Contents

  • Get started - how to add typesafe-i18n to your project
  • Usage - how to implement different use-cases
  • Typesafety - how to get the best typesafety features
  • Syntax - how to use the translation functions
  • Dictionary - how to structure your translations
  • Namespaces - how to optimize loading of your translations
  • Formatters - how to format dates and numbers
  • Switch-Case - how to output different words depending on an argument
  • Locale-detection - how to detect an user's locale
  • Utility functions - useful utility functions
  • Integrations - how to integrate other i18n services
  • Sizes - how much does typesafe-i18n add to your bundle size
  • Performance - how efficient is typesafe-i18n implemented
  • Sponsors - how to help this project grow
  • FAQs - how to get your questions answered

Get started

  1. โŒจ๏ธ Run the setup process and automatically detect the config needed

    npx typesafe-i18n --setup-auto

    or manually configure typesafe-i18n by answering a few questions

    npx typesafe-i18n --setup

    It didn't work? See here for possible troubleshooting.

  2. ๐Ÿ‘€ Take a look at the generated files and it's folder-structure after running npm run typesafe-i18n (or npx typesafe-i18n)

  3. ๐Ÿ“– Explore the assets

    typesafe-i18n offers a lot. Just press cmd + F to search on this page or see the table of contents that will link you to more specific subpages with more details.

  4. โญ Star this project on GitHub

    Thanks! This helps the project to grow.


Having trouble setting up typesafe-i18n? Reach out to us via Github Discussions or on Discord.

manual installation

npm install typesafe-i18n

changelog

The changelog of this project can be found here

migrations

Long-term goals

Curious about what comes next? See this discussion to learn more about the plans for the future of this project.

Contributions

If you would like to get involved within this project, take a look at this discussion.

Usage

The package can be used inside JavaScript and TypeScript applications. You will get a lot of benefits by running the generator since it will create a few wrappers to provide you with full typesafety.

You can use typesafe-i18n in a variety of project-setups:

Other frameworks

All you need is inside the generated file i18n-util.ts. You can use the functions in there to create a small wrapper for your application.

Feel free to open a new discussion if you need a guide for a specific framework.

Custom usage

See here if you want to learn how you can use typesafe-i18n to implement your own specific use-case.

Browser Support

The library should work in all modern browsers. It uses some functionality from the Intl namespace. You can see the list of supported browsers here. If you want to support older browsers that don't include these functions, you would need to include a polyfill like intl-pluralrules.

Typesafety

If you want to get the best typesafety features, you will need to use the generator in order to create types and boilerplate code for you

Here you can see some examples where typesafe-i18n can help you:

typesafe auto-completion for all your defined locales

typesafe locales completion

typesafe auto-completion for all available translations

typesafe translation key completion

you will get an error if you forget to pass arguments

typesafe number of arguments

you will get an error if you pass the wrong type arguments

typesafe arguments 1 typesafe arguments 2

you will get an error if you forgot to add a translation in a locale

typesafe keys in translations

you will get an error when a translation is missing an argument

typesafe arguments in translation

The typesafe-i18n package allows us to be 100% typesafe for our translation functions and even the translations for other locales itself. The generator outputs TypeScript definitions based on your base locale.

You will also benefit from full typesafe JavaScript code via JSDoc-annotations.

Integration with other services

typesafe-i18n comes with an API that allows other services to read and update translations. You can connect other services by using the importer and exporter functionality.

There also exists an official plugin for Inlang. It allows you to use typesafe-i18n together with the tooling Inlang provides. You can find it here.

Sizes

The footprint of the typesafe-i18n package is smaller compared to other existing i18n packages. Most of the magic happens in development mode, where the generator creates TypeScript definitions for your translations. This means, you don't have to ship the whole package to your users. The only two parts, that are needed in production are:

  • string-parser: detects variables, formatters and plural-rules in your localized strings
  • translation function: injects arguments, formats them and finds the correct plural form for the given arguments

These parts are bundled into the core functions. The sizes of the core functionalities are:

Apart from that there can be a small overhead depending on which utilities and wrappers you use.

There also exists a useful wrapper for some frameworks:

Performance

The package was optimized for performance:

  • the amount of network traffic is kept small
    The translation functions are small. Only the locales that are used are loaded
  • no unnecessary workload
    Parsing your translation file for variables and formatters will only be performed when you access a translation for the first time. The result of that parsing process will be stored in an optimized object and kept in memory.
  • fast translations
    Passing variables to the translation function will be fast, because its treated like a simple string concatenation. For formatting values, a single function is called per formatter.

If you use typesafe-i18n you will get a smaller bundle compared to other i18n solutions. But that doesn't mean, we should stop there. There are some possible optimizations planned to decrease the bundle size even further.

Sponsors

Become a sponsor โค๏ธ if you want to support my open source contributions.

ivanhofer's sponsors

Thanks for sponsoring my open source work!

FAQs


Dou you still have some questions? Reach out to us via Github Discussions or on Discord.


Calling LL.key() renders an empty string

You probably forgot to load the locale first before using it. Calling loadLocaleAsync('en') or loadAllLocales() will fix it.


Installing typesafe-i18n fails

Running the npx command with a npm version <7.0.0 will probably fail because it will not include peerDependencies.

You could try installing it locally via:

npm install typesafe-i18n

and then run the setup-command from within the node_modules folder via:

./node_modules/typesafe-i18n/cli/typesafe-i18n.mjs --setup-auto

here is the original issue with some additional information: #142


I added a new translation to my locale file, but TypeScript gives me the Error Property 'XYZ' does not exist on type 'TranslationFunctions'

Make sure to run the generator after you make changes to your base translation file. The generator will generate and update the types for you.


I don't use TypeScript, can I also use typesafe-i18n inside JavaScript applications?

Yes, you can. See the usage section for instructions. Even if you don't use TypeScript you can still improve from some typesafety features via JSDoc-annotations.


I added a new translation to my locale file, but the generator will not create new types

The generator will only look for changes in your base locale file. Make sure to always update your base locale file first, in order to get the correct auto-generated types. If you want to change your base locale file, make sure to give it the type of BaseTranslation. All other locales should have the type of Translation. E.g. if you set your base locale to italian, you would need to do it like this:

  • set your base locale to italian (it) in ยด.typesafe-i18n.json`:

    {
       "baseLocale": "it"
    }
  • define the type of your base locale as BaseTranslation

    // file 'src/i18n/it/index.ts'
    import type { BaseTranslation } from '../i18n-types'
    
    const it: BaseTranslation = {
       WELCOME: "Benvenuto!"
    }
    
    export default it
  • define the type of your other locales as Translation

    // file 'src/i18n/en/index.ts'
    import type { Translation } from '../i18n-types'
    
    const en: Translation = {
       WELCOME: "Welcome!"
    }
    
    export default en

The generator keeps overriding my changes I make to the i18n-files

The generator creates some helpful wrappers for you. If you want to write your own wrappers, you can disable the generation of these files by setting the generateOnlyTypes option to true.


Is typesafe-i18n supported by i18n-ally?

Yes, you can configure i18n-ally like this. There is currently also an open PR that will add official support for typesafe-i18n.


How can I access a translation dynamically?

When you want to dynamically access a translation, you can use the usual JavaScript syntax to access a property via a variable (myObject[myVariable]).

  1. define your translations
// i18n/en.ts
import type { BaseTranslation } from '../i18n-types'

const en: BaseTranslation = {
   category: {
      simple: {
         title: 'Simple title',
         description: 'I am a description for the "simple" category',
      },
      advanced: {
         title: 'Advanced title',
         description: 'I am a description for the "advanced" category',
      }
   }
}

export default en
  1. use it in your components
<script lang="ts">
   // Component.svelte

   import LL from '$i18n/i18n-svelte'
   import type { Translation } from '$i18n/i18n-types'

   // ! do not type it as `string`
   // by restricting the type, you don't loose the typesafety features
   export let category: keyof Translation['category'] = 'simple'
</script>

<h2>{$LL.category[category].title()}

<p>
   {$LL.category[category].description()}
<p>

How can I have translated validation messages?

Validation libraries like zod, yup, joi etc. usually provide a way to define custom error messages. You can use typesafe-i18n to translate these messages.

But you need to create the validation schema dynamically, after you have initialized the LL object ([]i18nObject](https://github.com/ivanhofer/typesafe-i18n/tree/main/packages/runtime#i18nObject)).

You can do that like this by passing the LL object to a function that returns the validation schema:

import { z } from 'zod'
import type { TranslationFunctions } from './i18n/i18n-types'

export const createLoginSchema = (LL: TranslationFunctions) => z.object({
    email: z.string().min(1, LL.validation.emptyField()).email(LL.validation.invalidEmail()),
    password: z.string().min(1, LL.validation.emptyField()),
})

How do I render a component inside a Translation?

By default typesafe-i18n at this time does not provide such a functionality. Basically you will need to write a function that splits the translated message and renders a component between the parts. You can define your split characters yourself but you would always need to make sure you add them in any translation since typesafe-i18n doesn't provide any typesafety for these characters (yet).


How can I use my base translation as a fallback for other locales?

With the strong typesafety features, you'll know if a locale is missing a translation. But in rare cases you might want to use your base translation as a fallback for other locales.

See the next FAQ entry. The same concept can be applied to prefill your translations with the base translation and then just override the parts that are translated.

You'll loose the some sort of typesafety with that approach since you can't know which parts are translated and which are not. Using the base translation as a fallback is not recommended because your UI will contain two different locales which might confuse your users.

I have two similar locales (only a few translations are different) but I don't want to duplicate my translations

Your locale translation files can be any kind of JavaScript object. So you can make object-transformations inside your translation file. The only restriction is: in the end it has to contain a default export with type Translation. You could do something like this:

  • create your BaseTranslation

    // file 'src/i18n/en/index.ts'
    import type { BaseTranslation } from '../i18n-types'
    
    const en: BaseTranslation = {
       WELCOME: "Welcome to XYZ",
       // ... some other translations
    
       COLOR: "colour"
    }
    
    export default en
  • create your other translation that overrides specific translations

    // file 'src/i18n/en-US/index.ts'
    import type { Translation } from '../i18n-types'
    import en from '../en' // import translations from 'en' locale
    
    const en_US: Translation = {
       ...en as Translation, // use destructuring to copy all translations from your 'en' locale
    
       COLOR: "color" // override specific translations
    }
    
    export default en_US

    If you are using nested translations, you should use the provided extendDictionary function that uses just-extend under the hood.

    import { extendDictionary } from '../i18n-utils'
    import en from '../en' // import translations from 'en' locale
    
    const en_US = extendDictionary(en, {
       labels: {
          color: "color" // override specific translations
       }
    })
    
    export default en_US

For certain locales I don't want to output a variable, but due to the strict typing I have to specify it in my translation

The generated types are really strict. It helps you from making unintentional mistakes. If you want to opt-out for certain translations, you can use the any keyword.

  • create your BaseTranslation with a translation containing a parameter

    // file 'src/i18n/en/index.ts'
    import type { BaseTranslation } from '../i18n-types'
    
    const en: BaseTranslation = {
       HELLO: "Hi {name}!",
    }
    
    export default en
  • create another locale without that parameter by disabling the strict type checking with as any

    // file 'src/i18n/de/index.ts'
    import type { Translation } from '../i18n-types'
    
    const de: Translation = {
       HELLO: "Hallo!" as any // we don't want to output the 'name' variable
    }
    
    export default de

WARNING! the usage of 'any' can introduce unintentional mistakes in future. It should only be used when really necessary and you know what you are doing.

A better approach would be to create a custom formatter e.g.

  • create your translation and add a formatter to your variable

    // file 'src/i18n/en/index.ts'
    import type { BaseTranslation } from '../i18n-types'
    
    const en: BaseTranslation = {
       HELLO: "Hi {name|nameFormatter}!",
    }
    
    export default en
    // file 'src/i18n/de/index.ts'
    import type { Translation } from '../i18n-types'
    
    const de: Translation = {
       HELLO: "Hallo {name|nameFormatter}!"
    }
    
    export default de
  • create the formatter based on the locale

    // file 'src/i18n/formatters.ts'
    import type { FormattersInitializer } from 'typesafe-i18n'
    import type { Locales, Formatters } from './i18n-types'
    import { identity, ignore } from 'typesafe-i18n/formatters'
    
    export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales)    => {
    
       const nameFormatter =
          locale === 'de'
             // return an empty string for locale 'de'
             ? ignore // same as: () => ''
             // return the unmodified parameter
             : identity // same as: (value) => value
    
       const formatters: Formatters = {
          nameFormatter: nameFormatter
       }
    
       return formatters
    }

Why does the translation function return a type of LocalizedString and not the type string itself?

With the help of LocalizedString you could enforce texts in your application to be translated. Lets take an Error message as example:

const showErrorMessage(message: string) => alert(message)

const createUser = (name: string, password: string) => {
   if (name.length === 0) {
      showErrorMessage(LL.user.create.nameNotProvided())
      return
   }

   if (isStrongPassword(password)) {
      showErrorMessage('Password is too weak')
      return
   }

   // ... create user in DB
}

In this example we can pass in any string, so it can also happen that some parts of your application are not translated. To improve your i18n experience a bit we can take advantage of the LocalizedString type:

import type { LocalizedString } from 'typesafe-i18n'

const showErrorMessage(message: LocalizedString) => alert(message)

const createUser = (name: string, password: string) => {
   if (name.length === 0) {
      showErrorMessage(LL.user.create.nameNotProvided())
      return
   }

   if (isStrongPassword(password)) {
      showErrorMessage('Password is too weak') // => ERROR: Argument of type 'string' is not assignable to parameter of type 'LocalizedString'.
      return
   }

   // ... create user in DB
}

With the type LocalizedString you can restrict your functions to only translated strings.


Tests are not running with Jest

Unfortunately there are some open issues in the Jest repository regarding modern package export formats so jest doesn't know where to load files from.

You need to manually tell jest where these files should be loaded from, by defining moduleNameMapper inside your jest.config.js:

// jest.config.js
module.exports = {
   moduleNameMapper: {
      "typesafe-i18n/angular": "typesafe-i18n/angular/index.cjs",
      "typesafe-i18n/react": "typesafe-i18n/react/index.cjs",
      "typesafe-i18n/solid": "typesafe-i18n/solid/index.cjs",
      "typesafe-i18n/svelte": "typesafe-i18n/svelte/index.cjs",
      "typesafe-i18n/vue": "typesafe-i18n/vue/index.cjs",
      "typesafe-i18n/formatters": "typesafe-i18n/formatters/index.cjs",
      "typesafe-i18n/detectors": "typesafe-i18n/detectors/index.cjs",
   }
};

here is the original issue with some additional information: #140


With Node.JS the Intl package does not work with locales other than 'en'

Node.JS, by default, does not come with the full intl support. To reduce the size of the node installment it will only include 'en' as locale. You would need to add it yourself. The easiest way is to install the intl package

> npm install intl

and then add following lines on top of your src/i18n/formatters.ts file:

const intl = require('intl')
intl.__disableRegExpRestore()
globalThis.Intl.DateTimeFormat = intl.DateTimeFormat

Then you should be able to use formatters from the Intl namespace with all locales.

Note: this is an older approach to the problem. You should not need this when using Node.js version > 16.


"Cannot find module" in yarn monorepo setup

Yarn uses a strange way to install dependencies in a monorepo setup. The issue lays in the "hoisting" of packages (see this issue). Therefore it might be that the typesafe-i18n dependencies cannot be found.

Changing the workspace config in package.json will fix the issue:

-  "workspaces": [
-    "apps/*",
-    "packages/*"
-  ],
+  "workspaces": {
+    "packages": [
+      "apps/*",
+      "packages/*"
+    ],
+    "nohoist": [
+      "**/typesafe-i18n",
+      "**/typesafe-i18n/**"
+    ]
+  },

typesafe-i18n's People

Contributors

accuser avatar angablue avatar canrau avatar chriscoomber avatar debashisbiswas avatar dependabot[bot] avatar derkoe avatar ekafyi avatar eldemarkki avatar estebanborai avatar flammae avatar gh-action-bump-version avatar glinkis avatar itkrt2y avatar ivanhofer avatar jesperp avatar mledl avatar mrnossiom avatar nkoehring avatar novacrazy avatar oscard0m avatar osdiab avatar personalyisus avatar samuelstroschein avatar scaltunsoy avatar snyk-bot avatar svenjacobs avatar vchirikov avatar whalemare avatar zalamar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

typesafe-i18n's Issues

Angular Adapter

A adapter for Angular or setup/sample for Angular would be helpful.

Generated types seem to be incorrect?

Describe the bug

I didn't do anything special as far as I'm aware. I just wanted to try out the HI translation that I saw in the readme.

So my base translation file, at /src/i18n/en/index.ts looks like this:

import type { BaseTranslation } from 'typesafe-i18n'

const en: BaseTranslation = {
  HI: "Hello {name:string}"
}

export default en

And then I tried to write the dutch translation file at /src/i18n/nl/index.ts like this:

import type { Translation } from "../i18n-types"

const nl: Translation = {
  HI: "Hallo {name:string}"
}

export default nl

And then I got the following error.

image

Reproduction

See the previous paragraph.

In case you're interested, this is what my /src/i18n/i18n-types.ts file looks like:

// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */

import type { LocalizedString } from 'typesafe-i18n'

export type BaseLocale = 'en'

export type Locales =
	| 'en'
	| 'nl'

export type Translation = {
	/**
	 * Hello {name}
	 * @param {string} name
	 */
	'HI': RequiredParams1<'name'>
}

export type TranslationFunctions = {
	/**
	 * Hello {name}
	 */
	'HI': (arg: { name: string }) => LocalizedString
}

export type Formatters = {}

type Param<P extends string> = `{${P}}`

type Params1<P1 extends string> =
	`${string}${Param<P1>}${string}`

type RequiredParams1<P1 extends string> =
	| Params1<P1>

I'm on Typescript 4.4.3 in case that matters.

Logs

No response

Config

{
  "adapter": "svelte",
  "loadLocalesAsync": false
}

Additional information

No response

Bug with string symbols in json

Describe the bug

Hi,

As soon as we use symbols in the json file with storeTranslationsToDisk, the output creates json formatting errors.

Thanks

Reproduction

Example with \n

{
  "example": "This is a\nexample."
}

to

const en: BaseTranslation = {
  "example": "This is a
example."
}

fix format

const en: BaseTranslation = {
  "example": `This is a
example.`
}

Logs

No response

Config

{
  "$schema": "https://unpkg.com/[email protected]/schema/typesafe-i18n.json",

  "baseLocale": "en",
  "adapter": "react",
  "loadLocalesAsync": false
}

Additional information

No response

Breaking Change: remove `.toString()` functionality

As described here, some frameworks allow you to only pass LL.KEY instead of LL.KEY(), when you don't have to pass arguments to your translation.
Its a small useful feature, that makes the syntax more appealing to read.
But it also causes a problem, when you later decide to update the translation to contain arguments. Typescript will no longer detect that you forgot to pass the required argument.

How this should be implemented:

  • remove it from the docs
  • show a console.warn message for a few weeks
  • remove .toString() functionality completely #35

Migration Guide:
add brackets for function invocation to all tranlsation calls, where it is missing e.g

- <p>{LL.WELCOME_MESSAGE}</p>
+ <p>{LL.WELCOME_MESSAGE()}</p>

watcher script is not compatible with yarn workspaces

After installing typesafe-i18n within one workspace, when you try to run the watcher:
node ./node_modules/typesafe-i18n/node/watcher.js
it does not work, since the files within node_modules are not physically located there (yarn does linking).

node ./node_modules/typesafe-i18n/node/watcher.js
internal/modules/cjs/loader.js:883
 throw err;
 ^

Error: Cannot find module '/Users/jakubwolny/projects/orca/client/node_modules/typesafe-i18n/node/watcher.js'
   at Function.Module._resolveFilename (internal/modules/cjs/loader.js:880:15)
   at Function.Module._load (internal/modules/cjs/loader.js:725:27)
   at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
   at internal/main/run_main_module.js:17:47 {
 code: 'MODULE_NOT_FOUND',
 requireStack: []
}

then of course I tried to run it in the directory above (project root), but when it watches entire project, not just single workspace, and paths are all wrong. Or I haven't found a way to tell watcher to specific dir to watch i.e.
node ./node_modules/typesafe-i18n/node/watcher.js --dir=client.
Also, from a perspective of a webpack user, I find it weird that I need to start another file watcher - I already have one (in webpack). So probably it's out of the scope of that issue, but having a webpack plugin could improve a Developer Experience.

So I haven't found any easy way to make it work within yarn workspace. btw. this is the most interesting i18n project I've seen in years I wanted to give a try.

Not supported on older browser versions due to Intl.PluralRules & Intl.Locale

https://caniuse.com/?search=PluralRules

My main phone is an iPhone 6S running 12.1.1. It's not able to load the webpage as it does not have Intl.PluralRules & Intl.Locale. As such I'm using polyfills for both:

import "@formatjs/intl-pluralrules/polyfill";
import "@formatjs/intl-locale/polyfill";

This isn't really a bug or problem with the library. But perhaps the library could either call an alternative method or provide a section with instructions on how to solve this issue on older browsers:

TypeError: undefined is not a constructor (evaluating 'new Intl.PluralRules(locale)')
getTranslateInstance
src/util.object.ts:20
  17 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
  18 | function i18nObject(locale, translations, formatters = {}) {
  19 |     return createProxy(getTranslateInstance(locale, translations, formatters));
20 | }
  21 | exports.i18nObject = i18nObject;
  22 | const createProxy = (fn, prefixKey) => new Proxy((prefixKey ? fn.bind(null, prefixKey) : {}), {
  23 |     get: (_target, key) => createProxy(fn, prefixKey ? ${prefixKey}.${key} : key),

EDIT: I updated to 2.34.0 and tried it again. On 2.34.0, it doesn't outright crash but instead it shows the translation label keys. In a way it's good because it still renders and is still usable. In other ways it's bad because it fails silently.

Using translations in a ts file

First I have to say the library is amazing.
Making my website for multiple languages, never been easier.

However I'm trying the use the translations inside a seperate .ts file for validations, and I can't figure out to do it properly.

An example:

export const required = (value: string | undefined) => {
  if (!value || value.length === 0) {
    return "must enter field";
  }
};

I'm trying to replace the string at the end with my translation.
Thanks in advance.

Import path on windows

Describe the bug

Hi,

I have a bug.
typesafe-i18n cannot find a path .

Thanks

Reproduction

I just follow the readme and test on a windows device in a library using webpack.
But I just run the script: "typesafe-i18n": "typesafe-i18n",

Logs

npm run typesafe-i18n

> [email protected] typesafe-i18n
> typesafe-i18n

[typesafe-i18n] generating files for TypeScript version: '4.4.x'
1\en\index.js Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only file and data URLs are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'c:'
[typesafe-i18n] ERROR: could not read default export from language file 'en'[typesafe-i18n] generated file: i18n-types.ts[typesafe-i18n] ... all files are up to date
[typesafe-i18n] files were modified => looking for changes ...
[typesafe-i18n] ERROR: import failed for C:\PATH\node_modules\typesafe-i18n\temp-output\12\en\index.js Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only file and data URLs are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'c:'
[typesafe-i18n] ERROR: could not read default export from language file 'en'[typesafe-i18n] ... all files are up to date

Config

{
  "$schema": "https://unpkg.com/[email protected]/schema/typesafe-i18n.json",

  "baseLocale": "en",
  "adapter": "react",
  "loadLocalesAsync": true
}

Additional information

No response

Type error in types/core.d.ts

Describe the bug

An index signature parameter type cannot be a union type. Consider using a mapped object type instead. ts(1337)

image

Reproduction

I'm using TS 4.3.2 in strict mode.
Unfortunately, I don't have a repo available for it right now.

Logs

No response

Config

{
   "baseLocale": "de",
   "adapter": "node",
   "outputPath": "./src",
   "loadLocalesAsync": false,
   "$schema": "https://unpkg.com/[email protected]/schema/typesafe-i18n.json"
}

Additional information

ts-config.json

{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "jsx": "react",
    "removeComments": true,
    "sourceMap": true,
    "esModuleInterop": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "target": "es6",
    "lib": [
      "es6"
    ],
    "incremental": false,
    "strict": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "resolveJsonModule": true,
    "outDir": "./lib"
  },
  "exclude": [
    "node_modules",
    "lib"
  ]
}

Named export 'i18nObject' not found

Named export 'i18nObject' not found. The requested module 'typesafe-i18n' is a CommonJS module

node: 15.8.0
vite: v2.2.4

"compilerOptions": {
	"moduleResolution": "node",
	"module": "es2020",
	"lib": ["es2020"],
	"target": "es2019",

.....

package.json: "type": "module"

bug: 2.32.0 broken import

Replicate:

  1. Checkout typesafe-i18n/examples/react
  2. Bump typesafe-i18n to 2.32.0
  3. npm install
  4. npm run
  5. Navigate to localhost:3000
util.object.ts:43 Uncaught TypeError: undefined is not a function
    at f (util.object.ts:43)
    at u (react-adapter.tsx:45)
    at Module.<anonymous> (i18n-react.tsx:9)
    at Module../src/i18n/i18n-react.tsx (i18n-react.tsx:19)
    at __webpack_require__ (bootstrap:851)
    at fn (bootstrap:150)
    at Module.<anonymous> (App.tsx:21)
    at Module../src/Child.tsx (Child.tsx:62)
    at __webpack_require__ (bootstrap:851)
    at fn (bootstrap:150)
    at Module.<anonymous> (App.css?dde5:82)
    at Module../src/App.tsx (App.tsx:21)
    at __webpack_require__ (bootstrap:851)
    at fn (bootstrap:150)
    at Module.<anonymous> (index.css?bb0a:82)
    at Module../src/index.tsx (index.tsx:8)
    at __webpack_require__ (bootstrap:851)
    at fn (bootstrap:150)
    at Object.1 (logo.svg:1)
    at __webpack_require__ (bootstrap:851)
    at checkDeferredModules (bootstrap:45)
    at Array.webpackJsonpCallback [as push] (bootstrap:32)
    at main.chunk.js:1
f @ util.object.ts:43
u @ react-adapter.tsx:45
(anonymous) @ i18n-react.tsx:9
./src/i18n/i18n-react.tsx @ i18n-react.tsx:19
__webpack_require__ @ bootstrap:851
fn @ bootstrap:150
(anonymous) @ App.tsx:21
./src/Child.tsx @ Child.tsx:62
__webpack_require__ @ bootstrap:851
fn @ bootstrap:150
(anonymous) @ App.css?dde5:82
./src/App.tsx @ App.tsx:21
__webpack_require__ @ bootstrap:851
fn @ bootstrap:150
(anonymous) @ index.css?bb0a:82
./src/index.tsx @ index.tsx:8
__webpack_require__ @ bootstrap:851
fn @ bootstrap:150
1 @ logo.svg:1
__webpack_require__ @ bootstrap:851
checkDeferredModules @ bootstrap:45
webpackJsonpCallback @ bootstrap:32
(anonymous) @ main.chunk.js:1
index.js:1 ./src/i18n/i18n-util.ts
Attempted import error: 'detectLocale' is not exported from 'typesafe-i18n/detectors' (imported as 'detectLocaleFn').
console.<computed> @ index.js:1
handleErrors @ webpackHotDevClient.js:174
push../node_modules/react-dev-utils/webpackHotDevClient.js.connection.onmessage @ webpackHotDevClient.js:213
index.js:1 ./src/App.tsx
Attempted import error: 'localStorageDetector' is not exported from 'typesafe-i18n/detectors'.
console.<computed> @ index.js:1
handleErrors @ webpackHotDevClient.js:174
push../node_modules/react-dev-utils/webpackHotDevClient.js.connection.onmessage @ webpackHotDevClient.js:213
index.js:1 ./node_modules/typesafe-i18n/adapters/adapter-react.mjs
Can't import the named export 'createContext' from non EcmaScript module (only default export is available)
console.<computed> @ index.js:1
handleErrors @ webpackHotDevClient.js:174
push../node_modules/react-dev-utils/webpackHotDevClient.js.connection.onmessage @ webpackHotDevClient.js:213
index.js:1 ./node_modules/typesafe-i18n/adapters/adapter-react.mjs
Can't import the named export 'useState' from non EcmaScript module (only default export is available)
console.<computed> @ index.js:1
handleErrors @ webpackHotDevClient.js:174
push../node_modules/react-dev-utils/webpackHotDevClient.js.connection.onmessage @ webpackHotDevClient.js:213
index.js:1 ./node_modules/typesafe-i18n/adapters/adapter-react.mjs
Can't import the named export 'useState' from non EcmaScript module (only default export is available)
console.<computed> @ index.js:1
handleErrors @ webpackHotDevClient.js:174
push../node_modules/react-dev-utils/webpackHotDevClient.js.connection.onmessage @ webpackHotDevClient.js:213
index.js:1 ./node_modules/typesafe-i18n/adapters/adapter-react.mjs
Can't import the named export 'useState' from non EcmaScript module (only default export is available)

Screenshot 2021-07-06 at 17 16 04

License

I tried out this library and it's great. I am not sure that I can use it in one of my projects though as I don't see any OSS licensing. Could you add a License file so we know what the licensing is?

Error: Cannot find module 'chokidar'

"typesafe-i18n": "^2.36.0"

i used to install npm i -D typesafe-i18n on my project and i wroted command script in my package.json

"scripts": {
    "i18n": "typesafe-i18n"
},

and i found like this error messages

Error: Cannot find module 'chokidar'
Require stack:
- /Users/hm/Desktop/Workspace/personal/polyger/node_modules/typesafe-i18n/node/generator.js
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:902:15)
    at Function.Module._load (internal/modules/cjs/loader.js:746:27)
    at Module.require (internal/modules/cjs/loader.js:974:19)
    at require (internal/modules/cjs/helpers.js:92:18)

Optional parameters in translations

Describe the bug

Optional parameters seem to be available, but they do not work, no value is passed along to the translation formatters.

Reproduction

// somewhere in the code
translation.dueDate({ dueDate: new Date() });

// the translation file
dueDate: "Project due date: {dueDate?:Date|date_DATE}",

// the formatter
date_DATE: (date) => {
  if (!date) {
    return "n/a";
  }
  return moment(date).format(MOMENT_FORMATS.DATE);
},

This will return Project due date: n/a although the types look correct and a date is sent along.

Logs

No response

Config

No response

Additional information

No response

The 2.49.0 version and 2.54.0 are critical incompatible. (silent shutdown..?)

Describe the bug

Many projects that installed this project lightly as npm i typesafe-i18n in version 2.49.0 are currently not working.
So I'm using the version of typesafe-i18n by lowering it to 2.49.0 version.

Can you check this out? I think this is fatal.

Reproduction

Initialize empty project of polyger (it based typesafe-i18n 2.49.0)

mkdir some_test
cd some_test
npm init -y
npm install [email protected]
npx polyger

The above command will be nothing executed and silently shutdown. (because some weird silent problem has been occoured in typesafe-i18n.)

then I'll install the previous version of typesafe-i18n and install it.

npm i [email protected]
npx polyger

and this one is working and showing some initialize messages. (if you can confirm it, just you can ctrl+c or command + c to shut it dowm.)

By executing the command below, you can check the previous version as it is.

npm i [email protected]
npx polyger

you can now seeing nothing to executed and can be confirm silent shutdown.

Logs

No response

Config

No response

Additional information

No response

Error: Cannot find module 'typesafe-i18n/formatters'

Now the error appears in developer mode.

yarn dev
Error when evaluating SSR module /src/i18n/formatters.ts:
Error: Cannot find module 'typesafe-i18n/formatters' from 'D:/app/src/i18n'

"typesafe-i18n": "^2.36.0"
"@sveltejs/kit": "^1.0.0-next.138",

2.53.2 breaks return type of i18nObject

Describe the bug

before 2.53.2 i18nObject return type is correctly equal to generated TranslationFunctions.
in > 2.53.2 it returns the wrong type

Reproduction

run i18nObject in pre / post version.

Logs

No response

Config

No response

Additional information

No response

support for deno

deno is an alternative to Node.js. It has TypeScript support out-of-the-box. It would be nice to be able to use typesafe-i18n with deno.

  • investigate what is needed to add support for deno
    • all imports need to include the file-ending *.ts:
      • update file generation to include file endings when outputting for deno (adapter: 'deno')
    • how can we import (only) types in deno??
      • find a solution to that problem
  • re-write watcher to be able to run with deno instead of node

Formatters typing generates one param type per translation file entry

First of all, thanks for a fantastic library, its fantastic to be able to mix TS and i18n in this way.

I'm running into a rather weird issue. The generated formatter type creates one input param type per translation file entry.

// i18n-types.ts

export type Formatters = {
	'requestTitle': (value: Request | Request) => unknown
	'userName': (value: User | User | User | User | User | User) => unknown
	'userNameAndOrg': (value: User) => unknown
}

As you can see, User | User | User | User | User | User. One User for every {sender:User|userName} in the translation file.

remove argument-types inside translations for production bundle

If you use types inside your base-translations, the type-information is also part of the generated output. Since this information is only needed for the watcher to output better types, it can be omitted in production builds to save a few bytes from the bundle.

We could automatically remove all argument types for production

if you have a translation like this:

export const en = {
   HI: 'Hello {name:string}!'
   // other translations
}

it could be optimize to the following code:

export const en = {
   HI: 'Hello {name}!'
   // other translations
}

What needs to be done:

  • simple implementation, assuming all translation-strings live inside the index.ts file
  • advanced implementation, where all strings e.g. from imports inside the index.ts are considered
  • (optional) create an updated string-parser that doesn't try to parse the type from the translations to improve output size by a few bytes

add webpack plugin

webpack users currently have to run the watcher via the node-process.

It would be useful if there existed a webpack plugin to improve the developer experience and let webpack users benefit from the bundling optimizations.

add `namespaces` functionality

Namespaces would be a native option to load only selected parts of the translations. Useful for bigger applications so not all messages need to load the same time e.g. translation that are only needed in the settings panel are not needed on the login page.

You would define your translations something like:

// index.ts
import type { Translation } from '../i18n-types'
import { workspace } from 'typesafe-i18n'

const en: Translation = {
   login: workspace('login')
   settings: workspace('settings')
   dashboard: {
      // ...
   }
}

export default en

// login.ts
import type { LoginTranslation } from '../i18n-types'

const login: LoginTranslation = {
   label_password: 'Password:',
   label_username: 'Username:',
   // ...
}

export default login

// settings.ts
import type { SettingsTranslation } from '../i18n-types'

const settings: SettingsTranslation = {
   email: 'Email preferences:',
   // ...
}

export default settings

where workspace would be a function that loads the workspace via an async import

const loadedWorkspaces = { }

export const workspace = async (workspaceToLoad: string) => {
  const ws = loadedWorkspaces[workspaceToLoad]
   if (ws) return ws

   loadWorkspace(workspaceToLoad)

   return { } // a dummy-workspace
}

export const loadWorkspace = async (workspaceToLoad: string) => {
   loadedWorkspaces[workspaceToLoad] = (await import(`./${workspaceToLoad}`)).default
}

Because workspaces would be loaded asynchronously, we would make sure that calling LL.login.label_password() would return the translated value: How can we do that?

  • call somewhere a loadWorkspace('login') function
    the function would need to get called before the actual translation call
  • show an empty string until the data finishes loading in background
    since the whole translation object is reactive, we can initialize the loading and as soon as the translations are back, we can update the object that holds all workspaces

A developer then can decide if he manually loads the correct workspace somewhere in the code (e.g. triggered by a certain route) or the user should see blanks until translations are loaded. Also a method to check if a workspace is currently loading will be exposed so a loading spinner can be shown.

It should be possible to nest workspaces as often an user wants.

What needs to be done:

  • create custom types that only include part of the workspace translations
  • make sure all translations define the part of the subtree as a workspace
  • implement reactivity for loading workspaces asynchronously
  • function to check if workspace is currently loading

Make an example for NextJS

Hi, I am trying to use your pack with NextJS :) It's so great but can you make an example to show what is the best option for nextjs?
I am using like this
In my _app.js

const App = ({ Component, pageProps, router }: AppProps) => {
    const detectedLocale = detectLocale(() => router.locales)
    
   return (
      <TypesafeI18n initialLocale={detectedLocale}>
            <Component {...pageProps} />
      </TypesafeI18n>
   )
}


in button-change-language

const LanguageChange = (props) => {
    const { LL, setLocale } = useContext(I18nContext)
    const router = useRouter()

 useEffect(() => {
        setLocale(router.locale as Locales) // change locale when router.locale change
    }, [router.locale])

return <button onclick={()=>router.push({
            pathname:router.asPath,
            locale:'it'

})}>click me to change</button>
}

use it

    const { LL } = useContext(I18nContext)

return (
<p>{ LL.login()}}</p>
)

Everything look work good but Is this the right way to use ?
I don't understand what is the benerfit of L,LL,LLL. is LL the best option?
Will the translate text fit SSR of NextJS, text will be static when run build ?

Broken in NextJs

Describe the bug

I started a new nextjs project and followed the install instructions, and somehow it doesn't work.

It's failing at runtime with an error "TypeError: e is undefined".

I also added a link to a repo with my setup. Am I doing something wrong with the scripts?

Reproduction

https://github.com/huntedman/broken-nextjs-i18n

Logs

No response

Config

No response

Additional information

No response

optimize output for production bundles

If you use keyed arguments inside your translations e.g. HI: Hello {name}!, the function to translate that message will also need to pass an object with the property name in it. It sure is more readable than passing index-based arguments, but it needs more characters. Depending of the amount and the length of the argument-property it will have a negative inpact on the generated bundle-size.

We could automatically rewrite arguments for production

if you have a translation like this:

export const en = {
   HI: 'Hello {name}!'
   // other translations
}

and use it inside your code like:

const message = LL.HI({ name: 'John' })

it could be optimize to the following code:

export const en = {
   HI: 'Hello {0}!'
   // other translations
}

const message = LL.HI('John')

Also the keys itself can be minified e.g. from 'HI' to 'a'

const message = LL.a('John')

What needs to be done:

  • write a rollup-plugin (using AST transforms)
    • rewrite all translations to use index-based arguments
    • rewrite all keys to use a shorter version (or an array with indices)
    • parse all translation calls and rewrite keyed arguments to index-based arguments

Best way to separate files

Hi, if translation file have thousands of lines. What will you do?
I am using like this, but it look like a good way.

import type { BaseTranslation } from 'typesafe-i18n'
import { langCheckOut } from './lang-checkout'

const en: BaseTranslation = {
    ...langCheckOut,
}

export default en

file: lang-checkout.ts

import { BaseTranslation } from 'typesafe-i18n'

export const langCheckOut: BaseTranslation = {
    clickToChangeLanguage: 'Click to change Language',
    login: 'Login',
    bankName: 'Bank name',
    transferAccordingToTheFollowingInformation:
        'Transfer according to the following information',
    accountNumber: 'Account number',
    loginToContinue: 'Login to continue',
    showPaymentInfo: 'Show Payment Info',
    choosePaymentMethod: 'Choose payment method',
    checkout: 'Checkout',
}


Support for sveltekit

Because of vitejs/vite#2579 it is currently not possible to use typesafe-i18n inside sveltekit applications.

A workaround is:

  • disabling ssr inside svelte.config.cjs
  • manually change export{u as getI18nSvelteStore}; to export const getI18nSvelteStore = u; inside node_modules\typesafe-i18n\adapters\adapter-svelte.js

Package path ./importer is not exported from package

Describe the bug

Hi,

The package path ./importer is not exported from package.

Thanks

Reproduction

I tried the example of imports with NextJs.

Logs

error - ./src/i18n/importer.ts:2:0
Module not found: Package path ./importer is not exported from package C:\PATH\node_modules\typesafe-i18n\package.json)
  1 | import ky from 'ky'
> 2 | import { storeTranslationToDisk } from 'typesafe-i18n/importer'
  3 |
  4 | const getDataFromAPI = async (locale: string) => {
  5 |   // this is just an example that returns static content

Import trace for requested module:
./pages\_app.tsx

https://nextjs.org/docs/messages/module-not-found

Config

{
  "$schema": "https://unpkg.com/[email protected]/schema/typesafe-i18n.json",

  "baseLocale": "en",
  "adapter": "react",
  "loadLocalesAsync": false
}

Additional information

No response

Generator not compatible with "type": "module".

Hello Ivan,

I switched to full ESM modules and everything works except the generated file i18n-util.ts.

The import statement isn't compatible with ESM, since it imports without the .js extension and tries to import a folder directly.

Maybe there is a way to add support via a flag. (like esmSupport)

Something like this at https://github.com/ivanhofer/typesafe-i18n/blob/main/packages/generator/src/files/generate-util.ts#L114

- import { initFormatters } from './${formattersTemplatePath}'
+ import { initFormatters } from './${formattersTemplatePath}${esmSupport ? '.js' : ''}'

I can make a pull request if you accept with all necessary types and code changes.

I'm at your disposal for any questions

MrNossiom
Thanks

warn if some translations-keys are not used anymore

Over time your application will grow. So do your translations. It can happen that you change a feature and add new translations, but forget to remove the old ones.

We could automatically look for unused translations

What needs to be done:

  • write a rollup-plugin
    • parse all files for used translation-keys
    • output warning, if a key is present in your translations, but not referenced in your code

react adapter is broken in version > 2.26.x

The target argument of the Proxy is set incorrectly. React doesn't like it, all other frameworks didn't complain. Will be fixed by correctly setting the target.

workaround: use version 2.25.x

transform bundle so no runtime parsing is needed

In its current implementation typesafe-i18n needs to parse each translation the first time it gets called. The parsing part is taking about half of the bundle size. The whole library could be trimmed to < 0.5 bytes if the parsing would take place compile-time.

Instead of this:

const en: Translation = {
   'HELLO': 'Hi {name:string}!'
}
export default en

the production bundle output could be:

const en: Translation = {
   'HELLO': ['Hi ', { k: 'name' }, '!']
}
export default en

This would mean slightly larger output for each translation file, but this should compress (gzip) better because of its repetitive pattern. If the parsing is not needed run-time, the library should be even faster (mostly nanoseconds).

What needs to be done:

  • evaluate the impact regarding bundle size
  • evaluate the time-savings if message parsing is not needed
  • implement an AST-transform function that parses and replaces each message
  • remove parsing part from output bundle

Default translations not defined on first render

Currently facing an issue where Jest fails to load translations despite having a baseLocale and initialLocale set.

The problem comes from react-adapater.tsx, where currentLocale and LL are not initialized on first render.

Normally this isn't an issue when viewing the webpage as it re-renders almost immediately, but Jest only captures the first render and fails due to it not being set.

I've modified the code to the following in my project as a temporary fix:

typesafe-i18n/packages/adapter-react/src/react-adapter.tsx

    const locale = props.initialLocale ?? baseLocale;

    const [isLoadingLocale, setIsLoadingLocale] = useState<boolean>(false);
    const [currentLocale, setCurrentLocale] = useState<L>(locale);
    const [LL, setLL] = useState<TranslationFunctions>(
      i18nObject<L, T, TF, F>(
        locale,
        (getTranslationForLocale as TranslationLoader<L, T>)(locale),
        initFormatters(locale) as any
      )
    );

Start node generator without watcher

Hi,
When testing my app, I'd like to generate the Translation class in order to check type safety with tsc. A script in package.json would look something like this:

"scripts": {
    "test": "typesafe-i18n && tsc && npm run testOnly",
    ...

But the problem is that the watcher keeps running and the script never continues executing. Would it be possible to have a one-shot mode of the typesafe-i18n command? I'd contribute if you don't have time.

'formatters.ts' not re-generated when changing 'loadLocaleAsync' option

Been experiencing weird bugs sometimes that weren't present in the examples/react.

My problems started from this line when I followed the React guide:

Configure typesafe-i18n by creating the file .typesafe-i18n.json with following contents:

{
   "adapter": "react"
}

This defaults to loadLocalesAsync: true, and generates an AsyncFormattersInitializer.

However after changing it to loadLocalesAsync: false, it never re-generated the file to a sync FormattersInitializer, which caused many weird bugs in my code.

I suppose this was intentional because formatters.ts is mostly user generated, and we wouldn't want to accidentally overwrite the dev's code.


a. One suggestion I can think of is to have formatters.ts only export const formatters: Formatters, and move initFormatters into i18n-util or a new file such as i18n-formatters.

b. Another suggestion I can think of is to remove the sync/async option from the config and always generate both, and let the developers pass in 'loadLocalesAsync' into initI18n. So it will now be controlled through code rather than config.

c. Third option is to simply add a note to update formatters.ts after changing the option.


On a side note, million thanks for creating this library and being so active in maintaining it! I've only had <50 translations to deal with so far, but it has already saved my team a ton of time! โค๏ธ

Import key format

Describe the bug

When I import the translate, the keys of the imported json file formatted like this "my-example" bug.

JSON file:

{
   "my-key": "example"
}

Import formated

import type { BaseTranslation } from '../i18n-types'

const en: BaseTranslation = {
  my-key: 'example', // instead of 'my-key': 'example',
}

export default en

Reproduction

Have a json file with a key with a dash.

Logs

No response

Config

No response

Additional information

No response

BREAKING: get rid of `locales` as generator-config

Originally posted by ivanhofer October 2, 2021
This option only makes sense inside the rollup-process, so it should only be available there.

Currently the rollup- and webpack-process allows to configure the generator by passing it as an argument. This option should be removed and the config should always be loaded from the .typesafe-i18n.json-file. This would mean a breaking change so a error-massage should point to a migration-guide.

Splitting translation into multiple files causes reload issues

To avoid a massive translation file I have opted to split it into parts e.g.

i18n/en/index.ts
i18n/en/publish.copy.ts
i18n/en/unPublish.copy.ts
...

If the copy is inside the index.ts file then any changes will be mirrored in i18n-types.ts rather quickly. But if I make changes to any of the other files (which in turn are included in index.ts) then the types are not updated. (when restarting typesafe it is updated however).

It should be noted that the library does pick up that files have been changed, but the types are not updated.

add an `importer` that makes it possible to import translations from another service

The importer would provide a function that can be called to store translations that come from another service like a translation service or a simple spreadsheet. This can be useful if the translations are handled from an external agency.

This could look something like:

import { storeLocalesToDisk } from 'typesafe-i18n/importer'

interface LocaleMapping {
   locale: string
   translations: {
      key: string
      message: string
   }
}

async function importData() {
   const dataFromAPI = await getDataFromSpreadsheed()

   const mappedData: LocaleMapping[] = mapData(dataFromAPI)

   storeLocalesToDisk(mappedData)
}

The data needs to be fetched, mapped and then passed to the storeLocalesToDisk function. The storeLocalesToDisk would then create or update the corresponding locale file in src/i18n/{locale}/index.ts.

The importer would also make it possible to import data from another message format like ICU and map them to the more compact message format typesafe-i18n uses.

generate optimized output for single locale exports

typesafe-i18n supports generating types only for a single locale by passing only the desired locale inside the locales watcher-option. This can be useful, if you want to create an own bundle per locale. Since these bundles don't have to include the switching of locales, the output size could be reduced.

add support for plural rule `{{zero|one|other}}`

Currently there exist two short forms for plural rules:

  • {0} apple{{s}} => plural only form
  • {0} {{apple|apples}} => singular/plural form

It would be useful, if there would also exist a third option:

  • {{no apples | 1 apple | {x} apples}}

What needs to be done:

  • implement parsing function for zero|one|other
  • add support for injecting passed parameter into plural part
    LLL('{{ no apples | 1 apple | ?? apples }}', 5) // => '5 apples'

changes to base translation not detected when using `outputFormat: 'JavaScript'`

Describe the bug

issue

The generator can't detect changes because it uses import to load the contents of the base translation file. The import statement gets cached so the process is not able to detect the changes.

possible solution

We can't just copy the file and import it from there to a new location because it may contain other imports.
Maybe we can also use TypeScript to "compile" the JavaScript and all imports into a temporary folder and import it from there.

workaround

restart the generator after making changes to the base translation

Reproduction

run the generator and make some changes to the base-translation file

Logs

No response

Config

{
   "outputFormat": "JavaScript"
}

Additional information

No response

create an `exporter` that makes it possible to export translations to an other service

Originally posted by ivanhofer October 13, 2021
As a counterpart to the importer there should also be the possibility to export translations to other services.

The exporter could look something like:

import { loadTranslationFromDisk, loadTranslationsFromDisk } from 'typesafe-i18n/exporter'

async function exportData() {
   // get all locales
   const translations = loadTranslationsFromDisk()
   // or just a single locale
   const translation = loadTranslationFromDisk('en')

   update(translations) // custom update functionality
}

Something broken in React Dev Tools

When I try to debug a component with useContext(I18nContext) (maybe) in React Dev Tools, a browser extension, I got an error like the below image (Uncaught TypeError: e.split is not a function) occurred.

2021 09 20_23 19 54

I don't know the cause of this error, and perhaps other people are getting the same error. Or is it a problem in my environment?

It can reproduce it by launching the React example ( https://github.com/ivanhofer/typesafe-i18n/tree/main/examples/react ) and opening the dev tools. (I checked in Chrome and Firefox and occurred error in both browser)

Consider providing support for Jest + React out of the box

Currently Jest is not able to run with typesafe-i18n out of the box.

The problem is caused by this line: import { initI18nReact } from 'typesafe-i18n/adapters/adapter-react', as adapter-react.js is compiled as a ESM file, however Jest is only able to process CJS files out of the box.

I was able to get Jest to run after changing packages/adapters/rollup.config.js to format: 'cjs' and rebuilding the files.

Although newer Jest versions can import ESM files directly, it is still experimental. Older versions require more configuration by using babel-transformers.

Consider compiling the file to CJS instead to provide out of the box support and to make it compatible with older versions without any additional configuration.

Let me know if I can do anything to help! :)

  โ— Test suite failed to run

    Jest encountered an unexpected token

    This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.

    By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".

    Here's what you can do:
     โ€ข If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/en/ecmascript-modules for how to enable it.
     โ€ข To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
     โ€ข If you need a custom transformation specify a "transform" option in your config.
     โ€ข If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.

    You'll find more details and examples of these config options in the docs:
    https://jestjs.io/docs/en/configuration.html

    Details:

    /Users/aelesia/Documents/git/project/node_modules/typesafe-i18n/adapters/adapter-react.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import t,{createContext as r,useState as e}from"react";const n=/\?\?/g,o=(t,r,e,o)=>{const l=o[0],i=l&&"object"==typeof l&&l.constructor===Object;return((t,r,e,o)=>t.map((t=>{if("string"==typeof t)return t;const{k:l="0",f:i=[]}=t,s=o[l];if((t=>!(!t.o&&!t.r))(t))return(("boolean"==typeof s?s?t.o:t.r:((t,{z:r,o:e,t:n,f:o,m:l,r:i},s)=>{switch(r&&0==s?"zero":t.select(s)){case"zero":return r;case"one":return e;case"two":return n;case"few":return o;case"many":return l;default:return i}})(r,t,s))||"").replace(n,s);const c=i.length?((t,r,e)=>r.reduce(((r,e)=>{var n,o;return null!==(o=null===(n=t[e])||void 0===n?void 0:n.call(t,r))&&void 0!==o?o:r}),e))(e,i,s):s;return(""+(null!=c?c:"")).trim()})).join(""))(t,r,e,1===o.length&&i?l:o)},l=t=>new Proxy(t?()=>t:{},{get:(r,e)=>l(t?`${t}.${e}`:e)}),i=/({?{[^\\}]+}}?)/g,s=(t,r=!0,e="",n="")=>t.split(i).filter(Boolean).map((t=>{if(!t.match(i))return t;const r=t.substring(1,t.length-1);if(r.startsWith("{")&&r.endsWith("}"))return((t,r)=>{let[e,n]=t.split(":");n||(n=e,e=r);const o=n.split("|"),[l,i,s,c,u,a]=o,f=o.filter((t=>void 0!==t)).length;return 1===f?{k:e,r:l}:2===f?{k:e,o:l,r:i}:3===f?{k:e,z:l,o:i,r:s}:{k:e,z:l,o:i,t:s,f:c,m:u,r:a}})(r.substring(1,r.length-1),n);const o=(t=>{const[r="",...e]=t.split("|"),[n="",o]=r.split(":");return{k:n,i:o,f:e}})(r);return n=o.k||n,!e&&(e=n),o})).map((t=>{if("string"==typeof t)return t;t.k||(t.k=e||"0");const n=(t=>Object.fromEntries(Object.keys(t).map((r=>{const e=t[r];return[r,Array.isArray(e)?e.map((t=>null==t?void 0:t.trim())):null==e?void 0:e.trim()]}))))(t);return r?(o=n,Object.fromEntries(Object.entries(o).map((([t,r])=>"i"!==t&&r&&"0"!=r&&[t,r])).filter(Boolean))):n;var o})),c=(t,r,e)=>{const n={},l=new Intl.PluralRules(t);return(t,...i)=>o(((t,r)=>t[r]||(t[r]=s(r)))(n,((t,r)=>{var e;return r.split(".").forEach((r=>t=t[r])),null!==(e=t)&&void 0!==e?e:r})(r,t)),l,e,i)};const u=(t,r)=>new Proxy(r?t.bind(null,r):{},{get:(e,n)=>u(t,r?`${r}.${n}`:n)}),a=(r="",n=(()=>({})),o=(()=>({})))=>{const i=f();return{component:s=>{const[a,f]=e(!1),[p,m]=e(null),[d,g]=e(l()),v=async t=>{f(!0);const r=n(t);g(function(t,r,e={}){return u(c(t,r,e))}(t,r,await o(t))),m(t),f(!1)};if(!p&&!a&&v(s.initialLocale||r),!a&&!d)return null;const h={setLocale:v,isLoadingLocale:a,locale:p,LL:d};return t.createElement(i.Provider,{value:h},s.children)},context:i}},f=()=>r({});export{a as initI18nReact};
                                                                                             ^^^^^^

    SyntaxError: Cannot use import statement outside a module

      2 | /* eslint-disable */
      3 |
    > 4 | import { initI18nReact } from 'typesafe-i18n/adapters/adapter-react'
        | ^
      5 | import type { Locales, Translation, TranslationFunctions, Formatters } from './i18n-types'
      6 | import { baseLocale, getTranslationForLocale } from './i18n-util'
      7 | import { initFormatters } from './formatters'

      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1350:14)
      at Object.<anonymous> (src/directory/i18n/i18n-react.tsx:4:1)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.