Giter Site home page Giter Site logo

i18n's Introduction

Nano Stores

A tiny state manager for React, React Native, Preact, Vue, Svelte, Solid, Lit, Angular, and vanilla JS. It uses many atomic stores and direct manipulation.

  • Small. Between 265 and 814 bytes (minified and brotlied). Zero dependencies. It uses Size Limit to control size.
  • Fast. With small atomic and derived stores, you do not need to call the selector function for all components on every store change.
  • Tree Shakable. A chunk contains only stores used by components in the chunk.
  • Designed to move logic from components to stores.
  • Good TypeScript support.
// store/users.ts
import { atom } from 'nanostores'

export const $users = atom<User[]>([])

export function addUser(user: User) {
  $users.set([...$users.get(), user]);
}
// store/admins.ts
import { computed } from 'nanostores'
import { $users } from './users.js'

export const $admins = computed($users, users => users.filter(i => i.isAdmin))
// components/admins.tsx
import { useStore } from '@nanostores/react'
import { $admins } from '../stores/admins.js'

export const Admins = () => {
  const admins = useStore($admins)
  return (
    <ul>
      {admins.map(user => <UserItem user={user} />)}
    </ul>
  )
}

  Made at Evil Martians, product consulting for developer tools.


Table of Contents

Install

npm install nanostores

Smart Stores

  • Persistent store to save data to localStorage and synchronize changes between browser tabs.
  • Router store to parse URL and implements SPA navigation.
  • I18n library based on stores to make application translatable.
  • Query store that helps you with smart remote data fetching.
  • Logux Client: stores with WebSocket sync and CRDT conflict resolution.

Devtools

  • Logger of lifecycles, changes in the browser console.
  • Vue Devtools plugin that detects stores and attaches them to devtools inspectors and timeline.

Guide

Atoms

Atom store can be used to store strings, numbers, arrays.

You can use it for objects too if you want to prohibit key changes and allow only replacing the whole object (like we do in router).

To create it call atom(initial) and pass initial value as a first argument.

import { atom } from 'nanostores'

export const $counter = atom(0)

In TypeScript, you can optionally pass value type as type parameter.

export type LoadingStateValue = 'empty' | 'loading' | 'loaded'

export const $loadingState = atom<LoadingStateValue>('empty')

Then you can use StoreValue<Store> helper to get store’s value type in TypeScript:

import type { StoreValue } from 'nanostores'

type Value = StoreValue<typeof $loadingState> //=> LoadingStateValue

store.get() will return store’s current value. store.set(nextValue) will change value.

$counter.set($counter.get() + 1)

store.subscribe(cb) and store.listen(cb) can be used to subscribe for the changes in vanilla JS. For React/Vue we have extra special helpers useStore to re-render the component on any store changes.

Listener callbacks will receive the updated value as a first argument and the previous value as a second argument.

const unbindListener = $counter.subscribe((value, oldValue) => {
  console.log(`counter value changed from ${oldValue} to ${value}`)
})

store.subscribe(cb) in contrast with store.listen(cb) also call listeners immediately during the subscription. Note that the initial call for store.subscribe(cb) will not have any previous value and oldValue will be undefined.

Maps

Map store can be used to store objects with one level of depth and change keys in this object.

To create map store call map(initial) function with initial object.

import { map } from 'nanostores'

export const $profile = map({
  name: 'anonymous'
})

In TypeScript you can pass type parameter with store’s type:

export interface ProfileValue {
  name: string,
  email?: string
}

export const $profile = map<ProfileValue>({
  name: 'anonymous'
})

store.set(object) or store.setKey(key, value) methods will change the store.

$profile.setKey('name', 'Kazimir Malevich')

Setting undefined will remove optional key:

$profile.setKey('email', undefined)

Store’s listeners will receive third argument with changed key.

$profile.listen((profile, oldProfile, changed) => {
  console.log(`${changed} new value ${profile[changed]}`)
})

You can also listen for specific keys of the store being changed, using listenKeys and subscribeKeys.

listenKeys($profile, ['name'], (value, oldValue, changed) => {
  console.log(`$profile.Name new value ${value.name}`)
})

subscribeKeys(store, keys, cb) in contrast with listenKeys(store, keys, cb) also call listeners immediately during the subscription. Please note that when using subscribe for store changes, the initial evaluation of the callback has undefined old value and changed key.

Deep Maps

Deep maps work the same as map, but it supports arbitrary nesting of objects and arrays that preserve the fine-grained reactivity.

import { deepMap, listenKeys } from 'nanostores'

export const $profile = deepMap({
  hobbies: [
    {
      name: 'woodworking',
      friends: [{ id: 123, name: 'Ron Swanson' }]
    }
  ],
  skills: [
    [
      'Carpentry',
      'Sanding'
    ],
    [
      'Varnishing'
    ]
  ]
})

listenKeys($profile, ['hobbies[0].friends[0].name', 'skills[0][0]'])

// Won't fire subscription
$profile.setKey('hobbies[0].name', 'Scrapbooking')
$profile.setKey('skills[0][1]', 'Staining')

// But those will fire subscription
$profile.setKey('hobbies[0].friends[0].name', 'Leslie Knope')
$profile.setKey('skills[0][0]', 'Whittling')

Note that setKey creates copies as necessary so that no part of the original object is mutated (but it does not do a full deep copy -- some sub-objects may still be shared between the old value and the new one).

Lazy Stores

A unique feature of Nano Stores is that every state has two modes:

  • Mount: when one or more listeners is mounted to the store.
  • Disabled: when store has no listeners.

Nano Stores was created to move logic from components to the store. Stores can listen for URL changes or establish network connections. Mount/disabled modes allow you to create lazy stores, which will use resources only if store is really used in the UI.

onMount sets callback for mount and disabled states.

import { onMount } from 'nanostores'

onMount($profile, () => {
  // Mount mode
  return () => {
    // Disabled mode
  }
})

For performance reasons, store will move to disabled mode with 1 second delay after last listener unsubscribing.

Call keepMount() to test store’s lazy initializer in tests and cleanStores to unmount them after test.

import { cleanStores, keepMount } from 'nanostores'
import { $profile } from './profile.js'

afterEach(() => {
  cleanStores($profile)
})

it('is anonymous from the beginning', () => {
  keepMount($profile)
  // Checks
})

Computed Stores

Computed store is based on other store’s value.

import { computed } from 'nanostores'
import { $users } from './users.js'

export const $admins = computed($users, users => {
  // This callback will be called on every `users` changes
  return users.filter(user => user.isAdmin)
})

An async function can be evaluated by using task().

import { computed, task } from 'nanostores'

import { $userId } from './users.js'

export const $user = computed($userId, userId => task(async () => {
  const response = await fetch(`https://my-api/users/${userId}`)
  return response.json()
}))

By default, computed stores update each time any of their dependencies gets updated. If you are fine with waiting until the end of a tick, you can use batched. The only difference with computed is that it will wait until the end of a tick to update itself.

import { batched } from 'nanostores'

const $sortBy = atom('id')
const $categoryId = atom('')

export const $link = batched([$sortBy, $categoryId], (sortBy, categoryId) => {
  return `/api/entities?sortBy=${sortBy}&categoryId=${categoryId}`
})

// `batched` will update only once even you changed two stores
export function resetFilters () {
  $sortBy.set('date')
  $categoryIdFilter.set('1')
}

Both computed and batched can be calculated from multiple stores:

import { $lastVisit } from './lastVisit.js'
import { $posts } from './posts.js'

export const $newPosts = computed([$lastVisit, $posts], (lastVisit, posts) => {
  return posts.filter(post => post.publishedAt > lastVisit)
})

Tasks

startTask() and task() can be used to mark all async operations during store initialization.

import { task } from 'nanostores'

onMount($post, () => {
  task(async () => {
    $post.set(await loadPost())
  })
})

You can wait for all ongoing tasks end in tests or SSR with await allTasks().

import { allTasks } from 'nanostores'

$post.listen(() => {}) // Move store to active mode to start data loading
await allTasks()

const html = ReactDOMServer.renderToString(<App />)

Store Events

Each store has a few events, which you listen:

  • onMount(store, cb): first listener was subscribed with debounce. We recommend to always use onMount instead of onStart + onStop, because it has a short delay to prevent flickering behavior.
  • onStart(store, cb): first listener was subscribed. Low-level method. It is better to use onMount for simple lazy stores.
  • onStop(store, cb): last listener was unsubscribed. Low-level method. It is better to use onMount for simple lazy stores.
  • onSet(store, cb): before applying any changes to the store.
  • onNotify(store, cb): before notifying store’s listeners about changes.

onSet and onNotify events has abort() function to prevent changes or notification.

import { onSet } from 'nanostores'

onSet($store, ({ newValue, abort }) => {
  if (!validate(newValue)) {
    abort()
  }
})

Event listeners can communicate with payload.shared object.

Integration

React & Preact

Use @nanostores/react or @nanostores/preact package and useStore() hook to get store’s value and re-render component on store’s changes.

import { useStore } from '@nanostores/react' // or '@nanostores/preact'
import { $profile } from '../stores/profile.js'

export const Header = ({ postId }) => {
  const profile = useStore($profile)
  return <header>Hi, {profile.name}</header>
}

Vue

Use @nanostores/vue and useStore() composable function to get store’s value and re-render component on store’s changes.

<script setup>
import { useStore } from '@nanostores/vue'
import { $profile } from '../stores/profile.js'

const props = defineProps(['postId'])

const profile = useStore($profile)
</script>

<template>
  <header>Hi, {{ profile.name }}</header>
</template>

Svelte

Every store implements Svelte's store contract. Put $ before store variable to get store’s value and subscribe for store’s changes.

<script>
  import { profile } from '../stores/profile.js'
</script>

<header>Hi, {$profile.name}</header>

In other frameworks, Nano Stores promote code style to use $ prefixes for store’s names. But in Svelte it has a special meaning, so we recommend to not follow this code style here.

Solid

Use @nanostores/solid and useStore() composable function to get store’s value and re-render component on store’s changes.

import { useStore } from '@nanostores/solid'
import { $profile } from '../stores/profile.js'

export function Header({ postId }) {
  const profile = useStore($profile)
  return <header>Hi, {profile().name}</header>
}

Lit

Use @nanostores/lit and StoreController reactive controller to get store’s value and re-render component on store’s changes.

import { StoreController } from '@nanostores/lit'
import { $profile } from '../stores/profile.js'

@customElement('my-header')
class MyElement extends LitElement {
  @property()

  private profileController = new StoreController(this, $profile)

  render() {
    return html\`<header>Hi, ${profileController.value.name}</header>`
  }
}

Angular

Use @nanostores/angular and NanostoresService with useStore() method to get store’s value and subscribe for store’s changes.

// NgModule:
import { NANOSTORES, NanostoresService } from '@nanostores/angular';

@NgModule({
  providers: [{ provide: NANOSTORES, useClass: NanostoresService }]
})
// Component:
import { Component } from '@angular/core'
import { NanostoresService } from '@nanostores/angular'
import { Observable, switchMap } from 'rxjs'

import { profile } from '../stores/profile'
import { IUser, User } from '../stores/user'

@Component({
  selector: "app-root",
  template: '<p *ngIf="(currentUser$ | async) as user">{{ user.name }}</p>'
})
export class AppComponent {
  currentUser$: Observable<IUser> = this.nanostores.useStore(profile)
    .pipe(switchMap(userId => this.nanostores.useStore(User(userId))))

  constructor(private nanostores: NanostoresService) { }
}

Vanilla JS

Store#subscribe() calls callback immediately and subscribes to store changes. It passes store’s value to callback.

import { $profile } from '../stores/profile.js'

$profile.subscribe(profile => {
  console.log(`Hi, ${profile.name}`)
})

Store#listen(cb) in contrast calls only on next store change. It could be useful for a multiple stores listeners.

function render () {
  console.log(`${$post.get().title} for ${$profile.get().name}`)
}

$profile.listen(render)
$post.listen(render)
render()

See also listenKeys(store, keys, cb) to listen for specific keys changes in the map.

Server-Side Rendering

Nano Stores support SSR. Use standard strategies.

if (isServer) {
  $settings.set(initialSettings)
  $router.open(renderingPageURL)
}

You can wait for async operations (for instance, data loading via isomorphic fetch()) before rendering the page:

import { allTasks } from 'nanostores'

$post.listen(() => {}) // Move store to active mode to start data loading
await allTasks()

const html = ReactDOMServer.renderToString(<App />)

Tests

Adding an empty listener by keepMount(store) keeps the store in active mode during the test. cleanStores(store1, store2, …) cleans stores used in the test.

import { cleanStores, keepMount } from 'nanostores'
import { $profile } from './profile.js'

afterEach(() => {
  cleanStores($profile)
})

it('is anonymous from the beginning', () => {
  keepMount($profile)
  expect($profile.get()).toEqual({ name: 'anonymous' })
})

You can use allTasks() to wait all async operations in stores.

import { allTasks } from 'nanostores'

it('saves user', async () => {
  saveUser()
  await allTasks()
  expect(analyticsEvents.get()).toEqual(['user:save'])
})

Best Practices

Move Logic from Components to Stores

Stores are not only to keep values. You can use them to track time, to load data from server.

import { atom, onMount } from 'nanostores'

export const $currentTime = atom<number>(Date.now())

onMount($currentTime, () => {
  $currentTime.set(Date.now())
  const updating = setInterval(() => {
    $currentTime.set(Date.now())
  }, 1000)
  return () => {
    clearInterval(updating)
  }
})

Use derived stores to create chains of reactive computations.

import { computed } from 'nanostores'
import { $currentTime } from './currentTime.js'

const appStarted = Date.now()

export const $userInApp = computed($currentTime, currentTime => {
  return currentTime - appStarted
})

We recommend moving all logic, which is not highly related to UI, to the stores. Let your stores track URL routing, validation, sending data to a server.

With application logic in the stores, it is much easier to write and run tests. It is also easy to change your UI framework. For instance, add React Native version of the application.

Separate changes and reaction

Use a separated listener to react on new store’s value, not an action function where you change this store.

  function increase() {
    $counter.set($counter.get() + 1)
-   printCounter(store.get())
  }

+ $counter.listen(counter => {
+   printCounter(counter)
+ })

An action function is not the only way for store to a get new value. For instance, persistent store could get the new value from another browser tab.

With this separation your UI will be ready to any source of store’s changes.

Reduce get() usage outside of tests

get() returns current value and it is a good solution for tests.

But it is better to use useStore(), $store, or Store#subscribe() in UI to subscribe to store changes and always render the actual data.

- const { userId } = $profile.get()
+ const { userId } = useStore($profile)

Known Issues

ESM

Nano Stores use ESM-only package. You need to use ES modules in your application to import Nano Stores.

In Next.js ≥11.1 you can alternatively use the esmExternals config option.

For old Next.js you need to use next-transpile-modules to fix lack of ESM support in Next.js.

i18n's People

Contributors

ai avatar andvlad avatar azat-io avatar begemot9i avatar beraliv avatar dartess avatar dmitrika avatar euaaaio avatar mikhin avatar nymless avatar zumkorn 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

i18n's Issues

Add HMR support

Now components will throw I18n component X was defined multiple times

number, time and count functions type mismatch

const { number, time } = useStore(format)

The documentation clearly says that they accept a second argument:

i18n/README.md

Lines 209 to 226 in 99d21a5

export const Date = (date) => {
let { time } = useStore(format)
return time(date)
}
```
These functions accepts options
of [`Intl.DateTimeFormat`] and [`Intl.NumberFormat`].
```ts
time(date, {
hour12: false,
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric'
}) //=> "November 1, 01:56:33"
```

But your typings do not agree with that:
image

Usage in sandboxes

I tried to put a demo with a working example in codesandbox and stackblitz, and in both cases I got Cannot use 'import.meta' outside a module. is it possible to wrap this part in try/catch?

is there a nice way to use nanostores/i18n outside of react / react-hooks?

I'm hoping to do something like this...

import fs from 'fs';
import { i18n } from '@nanostores/i18n';

i18n.loadStatic({
  'en-US': JSON.parse( fs.readFileSync('/path/to.en-US.json') ),
  'de-DE': JSON.parse( fs.readFileSync('/path/to.de-DE.json') )
});

i18n( 'en-US' )( 'click_here' ); // Click Here!

thanks for any reply you may have

Add partial translation loading

Now I18n downloads the whole translation JSON for specific locale.

get (locale) {
   return fetch(`/translations/${locale}.json`)
}

But many application parts are rarely used. It could be nice to have tree-shaking analog.

For instance, we use components name like main/post or settings/user. Then during the rendering we will collect all components names and send them to get. In this example, get will load translation for translations/ru/main.json and trabslations/settings/ru.json

get (locale, components) {
   return Promise.all(
    components
      .map(name => name.split('/')[0])
      .unique()
      .map(chunk => fetch(`/translations/${locale}/${chunk}.json`))
  )
}

Error while using useStore

Hello.

I'm using Preact + vite-plugin-ssr and useStore gives me an error:
TypeError: Cannot read properties of undefined (reading '__H')
So far, I haven't been able to figure out what the problem is. Maybe I'm doing something wrong.
Example

It would be great if anyone has any ideas
Thanks

Translation preprocessor

count/params can be explicitly applied to a specific message in translation.

It will be nice to have global translation preprocessor applied to all messages. For instance, we can create screen size transform on top of it:

// components/send-to-user.jsx
import { size } from '@nanostores/i18n'
import { i18n } from '../stores/i18n.js'

export messages = i18n({
  send: size({ // We need this wrap only for types, size() will return string in TS
    big: 'Send message',
    small: 'send'
  }),
  name: 'User name'
})
// translations/ru.json
{
  "send": "Отправить", // Only single variant here
  "name": { // In Russian we have variants in a different key
    "big": "Имя пользователя",
    "small": "Имя"
  }
}
// stores/i18n.js
import { atom, onMount } from 'nanostores'
import { createI18n, sizePrerocessor } from '@nanostores/i18n'

const screenSize = atom('big')
onMount(screenSize, () => {
  let media = window.matchMedia('(min-width: 600px)')
  funciton check () {
    screenSize.set(media.matches ? 'big' : 'small')
  }
  media.addEventListener('change', check)
  () => {
    media.removeEventListener('change', check)
  }
})

size.setSource(screenSize)

export const i18n = createI18n(locale, {
  get: ,
  preprocessors: [
    sizePrerocessor // I18n will listen for `size` and will re-build translations on `size.source` changes
  ]
})

Not able to change language

I am trying to change the language, but still the value of baseTranslation is used.
Am I dong something wrong? This is my implementation:

import { createI18n, localeFrom, browser } from '@nanostores/i18n'
import { persistentAtom } from '@nanostores/persistent'
import { fetchJson } from 'fetch-json'

export const setting = persistentAtom<string | undefined>('locale', undefined)

export const locale = localeFrom(
  setting,
  browser({
    available: ['en', 'pl'],
    fallback: 'en',
  }),
)

console.log(locale.get()) // logs en

export const i18n = createI18n(locale, {
  async get(code) {
    console.log('hello') // is never called
    if (code === 'pl') {
      return Promise.resolve({})
    }
    return fetchJson.get(`/translations/${code}.json`)
  },
})

export const navigation = i18n('navigation', {
  app: 'Aplikacja',
})

console.log(navigation.value?.app) // logs polish translation

Thank you!

Nesting of translations

const messages = i18n('post', {
  a: {
    b: {
      c: 'deep value',
    },
  },
});

Is this a valid construct or not?

If not, it should be described in the documentation.

If valid, we should fix the work of the functions and fix the types:

Now it works, but TS is throwing an error:

Type '{ b: { c: string; }; }' is not assignable to type 'Translation'.
  Object literal may only specify known properties, and 'b' does not exist in type 'TranslationFunction<any[], string | TranslationsJSON | TranslationFunction<any[], string | TranslationsJSON | TranslationFunction<any[], string | TranslationsJSON | TranslationFunction<any[], string | ... 1 more ... | TranslationFunction<...>>>>>'.(2322)

Also it doesn't work with functions:

const messages = i18n('post', {
  a: {
    b: {
      c: count({
        one: '{count} comment',
        many: '{count} comments',
      })
    },
  },
});
Uncaught TypeError: t.a.b.c is not a function

runtime error when NODE_ENV === 'production',

https://github.com/nanostores/i18n/blob/main/create-i18n/index.js#L23-L41

this is the stacktrace that appears in my service

TypeError: Cannot read properties of undefined (reading 'split')
    at file:///app/node_modules/@nanostores/i18n/create-i18n/index.js:68:34
    at listener (file:///app/node_modules/nanostores/lifecycle/index.js:129:19)
    at object.events.<computed>.reduceRight.shared (file:///app/node_modules/nanostores/lifecycle/index.js:17:58)
    at Array.reduceRight (<anonymous>)
    at file:///app/node_modules/nanostores/lifecycle/index.js:17:31
    at Object.store.listen (file:///app/node_modules/nanostores/lifecycle/index.js:136:9)

here's the nanostores location where the error occurs https://github.com/nanostores/i18n/blob/main/create-i18n/index.js#L68

the error appears to be a consequence of this failing condition. if this condition is not true, t.component is not defined and runtime error occurs when t.component.split('/')[0] is used later in the script

    if (process.env.NODE_ENV !== 'production') {
      t.component = componentName
      t.base = base
      if (define.cache[baseLocale][componentName]) {
        if (import.meta && (import.meta.hot || import.meta.webpackHot)) {
          /* c8 ignore next 3 */
          for (let i of define.cache) {
            delete define.cache[i][componentName]
          }
        } else {
          // eslint-disable-next-line no-console
          console.warn(
            `I18n component ${componentName} was defined multiple times. ` +
              'It could lead to cache issues. Try to move i18n definition ' +
              'from component’s render function.'
          )
        }
      }
    }

Prevent `baseTranslation` from flashing

I'm using SvelteKit, and so far it's working fine unless for 1 problem.
When refreshing the page, it always flashes the baseTranslation before the current locale translation.
The locale is persisted on the localStorage using persistentAtom.
I also added await translationsLoading(i18n) but same result.
And i18n.loading.get() always returning false

SSR Example

It would be very nice to see an example for SSR with for example nextjs

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.