Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,37 @@ if (isServer) {
export { locale, i18n }
```

### Loading translations asynchronously
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is use cases for that? SSR?

Why do we use it in React components?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's for rendering async server components which aren't updated on the client side. The translation must be available before server sends the rendered html to client.

async function Component(): Promise<React.ReactNode> {
 // ...
}

It's different from pre-rendered client components which are later hydrated with loaded translation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice explanation, let’s put it to the docs.


For rendering on the server where you're fine with waiting for the translations to load you can create a `loadTranslations` function:

```js
import { createI18n, createLoadTranslations } from '@nanostores/i18n'
import { atom } from 'nanostores'

const locale = atom('en')
const get = async (code, components) => {
return // your fetching logic
}

const i18n = createI18n(locale, { get })
const loadTranslations = createLoadTranslations(i18n, locale, get)
```

Then you can get translations by passing i18n component into this function:

```jsx
import { i18n, loadTranslations } from '../stores/i18n.js'

export const messages = i18n('post', {
post: 'Post details'
})

export const Post = async () => {
const t = await loadTranslations(messages)
return <span>{t.post}</span>
}
```

### Preprocessors

Expand Down
2 changes: 1 addition & 1 deletion create-i18n/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface Components {
}

export type Messages<Body extends Translations = Translations> =
ReadableAtom<Body>
{ component: string } & ReadableAtom<Body>

export interface I18n<Locale extends string = string> {
cache: Record<Locale, Translations>
Expand Down
3 changes: 2 additions & 1 deletion create-i18n/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ export function createI18n(locale, opts) {
}

let t = atom()
t.component = componentName

if (process.env.NODE_ENV !== 'production') {
t.component = componentName
t.base = base
if (define.cache[baseLocale][componentName]) {
let isHMR = import.meta && (import.meta.hot || import.meta.webpackHot)
Expand Down
37 changes: 37 additions & 0 deletions create-load-translations/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type {
I18n,
Messages,
TranslationLoader,
Translations
} from '../create-i18n/index.js'
import type { LocaleStore } from '../locale-from/index.js'

export interface LoadTranslations {
<Body extends Translations>(messages: Messages<Body>): Promise<Body>
}

/**
* Create a function to asynchronously load translations from i18n components.
*
* ```js
* import { createI18n, localeFrom, createLoadTranslations } from '@nanostores/i18n'
*
* const get = (code, components) => {
* // your fetching logic
* }
*
* export const locale = localeFrom(…)
* export const i18n = createI18n(locale, { get })
* export const loadTranslations = createLoadTranslations(i18n, locale, get)
* ```
*
* @param i18n Component definition function.
* @param locale Store with user’s locale.
* @param get Component loader function.
* @returns Asynchronous translations loader.
*/
export function createLoadTranslations<Locale extends string>(
i18n: I18n<Locale>,
locale: LocaleStore<Locale>,
get: TranslationLoader<Locale>
): LoadTranslations
17 changes: 17 additions & 0 deletions create-load-translations/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export function createLoadTranslations(i18n, locale, get) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to pass locale? I think we can use it from i18n.locale

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are completely right here, i didn't notice that i18n exposes locale

async function loadTranslations(messages) {
let code = locale.get()

let translations = await get(code, [messages.component])
if (Array.isArray(translations)) {
translations = translations.reduce((obj, item) =>
Object.assign(obj, item)
)
}

i18n.cache[code] = { ...i18n.cache[code], translations }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use cache do we need to check it (and return cached version is available)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add checks for it

return messages.get()
}

return loadTranslations
}
33 changes: 33 additions & 0 deletions create-load-translations/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { atom } from 'nanostores'
import { deepStrictEqual, equal } from 'node:assert'
import { test } from 'node:test'

import {
type ComponentsJSON,
createI18n,
createLoadTranslations
} from '../index.js'

test('waits for translations to load', async () => {
let locale = atom<'pl'>('pl')
Comment thread
ai marked this conversation as resolved.
Outdated

let get = async (): Promise<ComponentsJSON> => {
Comment thread
ai marked this conversation as resolved.
Outdated
return Promise.resolve({
component: { title: 'Tytuł' }
})
}

let i18n = createI18n(locale, { get })
let loadTranslations = createLoadTranslations(i18n, locale, get)

equal(i18n.loading.get(), false)

let messages = i18n('component', { title: 'Title' })

messages.subscribe(() => {})
equal(i18n.loading.get(), true)
deepStrictEqual(messages.get(), { title: 'Title' })

deepStrictEqual(await loadTranslations(messages), { title: 'Tytuł' })
equal(i18n.loading.get(), false)
})
4 changes: 4 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export {
Translations,
TranslationsJSON
} from './create-i18n/index.js'
export {
createLoadTranslations,
LoadTranslations
} from './create-load-translations/index.js'
export { eachMessage } from './each-message/index.js'
export { formatter, Formatter } from './formatter/index.js'
export { localeFrom, LocaleStore } from './locale-from/index.js'
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { browser } from './browser/index.js'
export { count } from './count/index.js'
export { createI18n } from './create-i18n/index.js'
export { createLoadTranslations } from './create-load-translations/index.js'
export { eachMessage } from './each-message/index.js'
export { formatter } from './formatter/index.js'
export { localeFrom } from './locale-from/index.js'
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@
{
"name": "Minimum",
"import": "{ localeFrom, createI18n }",
"limit": "651 B"
"limit": "665 B"
},
{
"name": "Maximum",
"import": "{ localeFrom, browser, createI18n, params, count, formatter, createProcessor }",
"limit": "1059 B"
"import": "{ localeFrom, browser, createI18n, params, count, formatter, createProcessor, createLoadTranslations }",
"limit": "1122 B"
}
],
"clean-publish": {
Expand Down
Loading