ITADN
nanostores/i18n · 文件 下载 ZIP
文件最后提交记录最后更新时间
README.md
以下内容由 AI 翻译,如有问题请点此提交 issue 反馈

Nano Stores I18n

一个小巧且灵活的 JS 库,用于让您的 Web 应用可翻译。 使用 Nano Stores 状态管理器和 JS Internationalization API

  • 小巧。 约 1 KB(压缩并 Brotli 编码后)。零依赖。
  • 支持 ReactPreactVueSvelte 以及纯 JS。
  • 支持 tree-shaking 和翻译的按需下载
  • 兼容在线翻译服务(如 Weblate)的纯扁平 JSON 翻译。
  • 开箱即用的翻译 TypeScript 支持。
  • 灵活的变量翻译。您可以更改翻译, 例如,根据屏幕大小而定。
// components/post.jsx
import { params, count } from '@nanostores/i18n' // You can use own functions
import { useStore } from '@nanostores/react'
import { i18n, format } from '../stores/i18n.js'

export const messages = i18n('post', {
  title: 'Post details',
  published: params('Was published at {at}'), // TypeScript will get `at` type
  comments: count({
    one: '{count} comment',
    other: '{count} comments'
  })
})

export const Post = ({ author, comments, publishedAt }) => {
  const t = useStore(messages)
  const { time } = useStore(format)
  return (
    <article>
      <h1>{t.title}</h1>
      <p>{t.published({ at: time(publishedAt) })}</p>
      <p>{t.comments(comments.length)}</p>
    </article>
  )
}
// stores/i18n.js
import { createI18n, localeFrom, browser, formatter } from '@nanostores/i18n'
import { persistentAtom } from '@nanostores/persistent'

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

export const locale = localeFrom(
  setting, // User’s locale from localStorage
  browser({
    // or browser’s locale auto-detect
    available: ['en', 'fr', 'ru'],
    fallback: 'en'
  })
)

export const format = formatter(locale)

export const i18n = createI18n(locale, {
  get(code) {
    return fetchJSON(`/translations/${code}.json`)
  }
})
// public/translations/ru.json
{
  "post": {
    "title": "Данные о публикации",
    "published": "Опубликован {at}",
    "comments": {
      "one": "{count} комментарий",
      "few": "{count} комментария",
      "many": "{count} комментариев",
    }
  },
  // Translations for all other components
}

Nano Stores I18n 由 Evil Martians 开发,这是一家专注于 开发者工具、AI 和网络安全初创企业 的美国设计与工程咨询公司。


安装

npm install nanostores @nanostores/i18n

对于 Astro,你还需要 astro-nanostores-i18n

用法

我们将 locale、时间/数字格式化函数和翻译 存储在 Nano Stores 的 atoms 中。请参阅 Nano Stores docs 以了解如何在你的框架中使用 atoms。

区域设置

区域设置是用户语言和方言的代码,例如 hi(印地语)、de-AT (奥地利使用的德语)。我们使用 Intl locale format

当前区域设置应存储在 store 中。我们有 localeFrom() store 构建器,用于在第一个可用来源中查找用户的区域设置:

import { localeFrom } from '@nanostores/i18n'

export const locale = localeFrom(store1, store2, store3)

我们有一个来自浏览器设置的 locale 的 store。你需要传递应用程序可用翻译的列表。如果 store 找不到公共 locale,它将使用回退 locale(en,但可以通过 fallback 选项更改)。

import { localeFrom, browser } from '@nanostores/i18n'

export const locale = localeFrom(
  …,
  browser({ available: ['en', 'fr', 'ru'] as const })
)

browser 存储之前,你可以放置一个存储,这将允许用户手动覆盖 区域设置。例如,你可以在 localStorage 中保留区域设置的覆盖。

import { persistentAtom } from '@nanostores/persistent'

const LOCALES = ['en', 'fr', 'ru'] as const
type Locale = (typeof LOCALES)[number]

export const localeSettings = persistentAtom<Locale>('locale', 'en')

export const locale = localeFrom(
  localeSettings,
  browser({ available: LOCALES })
)

或者你可以从 URL 路由器中获取用户的区域设置:

import { computed } from 'nanostores'
import { router } from './router.js'

const LOCALES = ['en', 'fr', 'ru'] as const
type Locale = (typeof LOCALES)[number]

function validate(locale: string): Locale {
  return LOCALES.includes(locale) ? locale : 'en'
}

const urlLocale = computed(router, page => validate(page?.params.locale))

export const locale = localeFrom(urlLocale, browser({ available: LOCALES }))

您可以将 locale 用作任何 Nano Store:

import { useStore } from '@nanostores/react'
import { locale } from '../stores/i18n.js'

// Pure JS example
locale.listen(code => {
  console.log(`Locale was changed to ${code}`)
})

// React example
export const CurrentLocale = () => {
  let code = useStore(locale)
  return `Your current locale: ${code}`
}

对于测试,你可以使用简单的原子:

import { atom } from 'nanostores'

const locale = atom('en')
locale.set('fr')

日期、数字与相对时间格式

formatter() 创建一个用于格式化数字和时间的 store。

import { formatter } from '@nanostores/i18n'

export const format = formatter(locale)

该商店将具有 time()number()relativeTime() 功能。

import { useStore } from '@nanostores/react'
import { format } from '../stores/i18n.js'

export const Date = date => {
  let { time } = useStore(format)
  return time(date)
}

这些函数接受 Intl.DateTimeFormatIntl.NumberFormatIntl.RelativeTimeFormat 选项。

time(date, {
  hour12: false,
  month: 'long',
  day: 'numeric',
  hour: 'numeric',
  minute: 'numeric'
}) //=> "November 1, 01:56:33"

relativeTime(-1, 'day', { numeric: 'auto' }) //=> "yesterday"

I18n 对象

I18n 对象用于定义新组件,并在语言环境变更时下载翻译。

import { createI18n } from '@nanostores/i18n'

export const i18n = createI18n(locale, {
  async get(code) {
    return await fetchJSON(`/translations/${code}.json`)
  }
})

在每个组件中,你都会拥有包含函数和类型的基础翻译。 此翻译不会从服务器下载。默认情况下,你应该 使用英语。你可以在组件中通过 baseLocale 选项更改基础语言环境。

翻译

我们有两种类型的翻译:

基础翻译。 开发者在组件源代码中编写它。它用于 TypeScript 类型和翻译函数(count()params() 等)。

export const messages = i18n('post', {
  title: 'Post details',
  published: params('Was published at {at}'),
  comments: count({
    one: '{count} comment',
    other: '{count} comments'
  })
})

其他翻译 它们使用 JSON 格式,将由翻译者创建。

{
  "post": {
    "title": "Данные о публикации",
    "published": "Опубликован {at}",
    "comments": {
      "one": "{count} комментарий",
      "few": "{count} комментария",
      "many": "{count} комментариев"
    }
  }
}

翻译应采用扁平结构(键 → 译文),不包含嵌套键。复数形式(count())和其他辅助功能不会引入额外的嵌套,因为它们被视为一种翻译。

参数

params() translation transform 替换 translation string 中的参数。

import { useStore } from '@nanostores/react'
import { params } from '@nanostores/i18n'
import { i18n } from '../stores/i18n.js'

export const messages = i18n('hi', {
  hello: params('Hello, {name}')
})

export const Robots = ({ name }) => {
  const t = useStore(messages)
  return t.hello({ name })
}

你可以使用 time()number()relativeTime() [格式化函数]。

您还可以使用 count() 函数:

import { count, params } from '@nanostores/i18n'
import { i18n } from '../stores/i18n'

export const messages = i18n('pagination', {
  page: params<{ category: string }>(
    count({
      one: 'One page in {category}',
      other: '{count} pages in {category}'
    })
  )
})

export const RobotsListInfo = ({ count }) => {
  const t = useStore(messages)
  return t.page({ category: 'robots' })(count)
}

复数形式

在许多语言中,文本可能因项目数量不同而有所差异。 请比较英语中的 1 robot/2 robots 与 波兰语中的 1 robot/2 roboty/2.5 robota/10 robotów

我们通过 count() 翻译转换来隐藏这种复杂性:

import { useStore } from '@nanostores/react'
import { count } from '@nanostores/i18n'
import { i18n } from '../stores/i18n.js'

export const messages = i18n('robots', {
  howMany: count({
    one: '{count} robot',
    other: '{count} robots'
  })
})

export const Robots = ({ robots }) => {
  const t = useStore(messages)
  return t.howMany(robots.length)
}
{
  "robots": {
    "howMany": {
      "one": "{count} robot",
      "few": "{count} roboty",
      "many": "{count} robotów",
      "other": "{count} robota"
    }
  }
}

count() 使用 Intl.PluralRules 获取每个区域设置的复数规则。

自定义变量翻译

除了 params()count(),你还可以定义自己的翻译 转换。或者,你可以通过替换 count()params() 来更改复数形式或参数语法。

import { transform, strings } from '@nanostores/i18n'

// Add parameters syntax like hello: "Hi, %1"
export const paramsList = transform((locale, translation, ...args) => {
  return strings(translation, str => {
    return str.replace(/%\d/g, pattern => args[pattern.slice(1)])
  })
})
import { paramsList } from '../lib/paramsList.ts'

export const messages = i18n('hi', {
  hello: paramsList('Hello, %1')
})

翻译流程

良好的 I18n 支持不在于 I18n 库, 而在于翻译流程。

  1. 开发者在组件源码中创建基础翻译,并将其导出 为 messages

    export const messages = i18n('welcome', {
      hello: params('Hello, %1')
    })
  2. CI 运行脚本以将基础翻译提取为 JSON。

    import { messagesToJSON } from '@nanostores/i18n'
    
    const components = await glob('./src/*.tsx', { absolute: true })
    const translations = await Promise.all(
      components.map(async file => {
        return await import(file).messages // Replace import if you export
        // i18n() result with a different name
      })
    )
    const json = messagesToJSON(...translations)
  3. CI 将包含基础翻译的 JSON 上传到在线翻译服务。

  4. 译员在该服务上翻译应用程序。

  5. CI 或翻译服务将翻译 JSON 下载到项目中。

懒加载

在一般情况下,开发者像这样传递 get 函数,以在语言环境变更时获取所有 翻译。

export const i18n = createI18n(locale, {
  async get(code) {
    return fetchJSON(`/translations/${code}.json`)
  }
})

然后使用 i18n 定义 post 组件。

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

许多应用部件很少使用,因此存在一种方式可以部分获取它们的翻译。

  1. 我们可以使用诸如 main/postsettings/user 这样的组件名称。

    export const messages = i18n('main/post', {
      post: 'Post details'
    })
  2. 我们可以定义组件更常用,并赋予它们相同的 前缀,例如 main/headingmain/postmain/comment

  3. 翻译应命名为:

    // public/translations/ru/main.json
    {
      "main/post": {
        "post": "Данные о публикации"
      },
      "main/heading": {
        "heading": "Заголовок"
      },
      "main/comment": {
        "comment": "Комментарий"
      }
    }
    // public/translations/ru/settings.json
  4. 在渲染期间,i18n 会保存所有已使用的组件名称。 当语言环境更改时,i18n 将这些名称发送给 get 函数。

  5. 我们可以传递一个 get 函数,该函数用于拆分前缀、过滤其中的唯一值, 并为所需的翻译发起请求。

    ```ts
    export const i18n = createI18n(locale, {
      async get(code, components) {
        let prefixes = components.map(name => name.split('/')[0])
        let unique = Array.from(new Set(prefixes))
        return Promise.all(
          unique.map(chunk =>
            fetchJSON(`/translations/${code}/${chunk}.json`)
          )
        )
      }
    })
    ```
    
  6. 在每次新的渲染后,i18n 会检查缓存中的翻译。 如果不在缓存中: _ 拆分组件的唯一前缀,或获取不带前缀的名称。 _ 检查是否已请求其翻译,但尚未收到响应。 * 如有需要,为组件名称调用 get 函数 - mainsettings

  7. 对于所有具有唯一名称的新渲染组件,都会调用 Fetch。为了防止这种情况,我们可能希望为它们设置相同的前缀。

Server-Side Rendering

对于 SSR,你可能希望使用自己的 locale store,并在自定义的 i18n 中设置 cache 选项,以避免加载翻译:

import { createI18n } from '@nanostores/i18n'
import { atom } from 'nanostores'

let locale, i18n

if (isServer) {
  locale = atom(db.getUser(userId).locale || parseHttpLocaleHeader())
  i18n = createI18n(locale, {
    async get () {
      return {}
    },
    cache: {
      fr: frMessages
    }
  })
} else {
  …
}

export { locale, i18n }

仅服务端渲染

当完全在服务端渲染内容而不进行客户端水合时, 你可以创建一个 loadTranslations 辅助函数。它确保翻译 在你使用它们之前已加载。

// components/post.jsx
import { loadTranslations } from '@nanostores/i18n'
import { i18n } from '../stores/i18n.js'

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

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

预处理器

你可以通过预处理器更改翻译中的所有消息。

例如,你可以应用排版规则。

import { createI18n, eachMessage } from '@nanostores/i18n'

export const i18n = createI18n(locale, {
  …
  preprocessors: [
    eachMessage(str => str.toLocaleLowerCase())
  ]
})

处理器

你可以注册自定义类型,用于根据某些状态选择翻译 (并在状态变化时更改翻译)。

例如,以下是一个根据屏幕大小更改翻译的示例:

// stores/i18n.js
import { atom, onMount } from 'nanostores'
import { createI18n, createProcessor } from '@nanostores/i18n'

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

export const size = createProcessor(screenSize)

export const i18n = createI18n(locale, {
  get: …,
  processors: [
    size
  ]
})
// components/send-to-user.jsx
import { i18n, size } from '../stores/i18n.js'

export const messages = i18n({
  send: size({
    big: 'Send message',
    small: 'send'
  }),
  name: 'User name'
})

export const SendLabel = () => {
  const t = useStore(messages)
  return t.send()
}