Skip to content

Commit

Permalink
feat: Initial release!
Browse files Browse the repository at this point in the history
  • Loading branch information
Mark Skelton committed Mar 26, 2021
1 parent 8ec44fd commit 37eb5c2
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
"env": {
"jest": true,
"node": true
},
"rules": {
"@typescript-eslint/no-non-null-assertion": "off"
}
}
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Build](https://github.com/Widen/i18next-async-backend/actions/workflows/build.yml/badge.svg)](https://github.com/Widen/i18next-async-backend/actions/workflows/build.yml)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)

i18next backend which loads resources via promises. Useful when loading resources via dynamic imports.
i18next backend which loads resources via promises. Useful when loading resources via dynamic imports.

## Installation

Expand All @@ -21,7 +21,18 @@ yarn add i18next-async-backend

## Usage

TODO
```js
import i18next from 'i18next'
import AsyncBackend from 'i18next-async-backend'

i18next.use(AsyncBackend).init({
backend: {
resources: {
en: {},
},
},
})
```

## Releasing

Expand Down
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
{
"name": "i18next-async-backend",
"version": "0.0.0-semantically-released",
"description": "i18next backend which loads resources via promises. Useful when loading resources via dynamic imports.",
"description": "i18next backend which loads resources via promises. Useful when loading resources via dynamic imports.",
"author": "Widen",
"license": "ISC",
"repository": "github:Widen/i18next-async-backend",
"homepage": "https://github.com/Widen/i18next-async-backend#readme",
"bugs": {
"url": "https://github.com/Widen/i18next-async-backend/issues"
},
"keywords": [],
"keywords": [
"i18next",
"i18next-backend",
"i18next-plugin",
"dynamic-import"
],
"exports": "./lib/index.js",
"types": "lib/index.d.ts",
"files": [
Expand All @@ -26,11 +31,15 @@
"main"
]
},
"peerDependencies": {
"i18next": ">=20"
},
"devDependencies": {
"@types/jest": "^26.0.22",
"@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0",
"eslint": "^7.22.0",
"i18next": "^20.1.0",
"jest": "^26.6.3",
"prettier": "^2.2.1",
"semantic-release": "^17.4.2",
Expand Down
48 changes: 46 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,47 @@
export function run(): boolean {
return true
import { BackendModule, ReadCallback, Services } from 'i18next'

type ResourceFetcher = () => Promise<Record<string, string>>

export interface AsyncBackendOptions {
resources?: {
[language: string]: ResourceFetcher | Record<string, ResourceFetcher>
}
}

export default class AsyncBackend
implements BackendModule<AsyncBackendOptions> {
// i18next is dumb as TypeScript requires the class property for `type`
// but the runtime requires the static `type` property.
static type = 'backend'
type = 'backend' as const

private options: AsyncBackendOptions = null!

constructor(services: Services, options: AsyncBackendOptions) {
this.init(services, options)
}

init(_: Services, options: AsyncBackendOptions): void {
this.options = { ...this.options, ...options }
}

read(lng: string, ns: string, callback: ReadCallback): void {
const resourceFetcher = this.getResourceFetcher(lng, ns)

if (resourceFetcher) {
resourceFetcher()
.then((resource) => callback(null, resource))
.catch((err) => callback(err, false))
} else {
callback(new Error('resource not found'), false)
}
}

private getResourceFetcher(lng: string, ns: string) {
// Languages can specify a function if they only have a single namespace
// or an object if they have multiple namespaces.
const fetcher = this.options.resources?.[lng]

return typeof fetcher === 'function' ? fetcher : fetcher?.[ns]
}
}
60 changes: 60 additions & 0 deletions test/AsyncBackend.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import i18next from 'i18next'
import AsyncBackend, { AsyncBackendOptions } from '../src'

async function init(ns: string[], resources: AsyncBackendOptions['resources']) {
const i18n = i18next.createInstance().use(AsyncBackend)

const t = await i18n.init({
backend: { resources },
fallbackLng: 'en',
defaultNS: ns[0],
ns,
})

return { i18n, t }
}

it('should accept a function if there is only one namespace', async () => {
const { t } = await init(['translation'], {
en: () => Promise.resolve({ foo: 'bar' }),
})

expect(t('foo')).toBe('bar')
})

it('should accept an object if there are multiple namespaces', async () => {
const { t } = await init(['ns1', 'ns2'], {
en: {
ns1: () => Promise.resolve({ fruit: 'Apples' }),
ns2: () => Promise.resolve({ fruit: 'Oranges' }),
},
})

expect(t('fruit')).toBe('Apples')
expect(t('ns1:fruit')).toBe('Apples')
expect(t('ns2:fruit')).toBe('Oranges')
})

it('should load multiple languages', async () => {
const { t, i18n } = await init(['ns1', 'ns2'], {
en: {
ns1: () => Promise.resolve({ fruit: 'Apples' }),
ns2: () => Promise.resolve({ fruit: 'Oranges' }),
},
es: {
ns1: () => Promise.resolve({ fruit: 'Manzanas' }),
ns2: () => Promise.resolve({ fruit: 'Naranjas' }),
},
})

// English
expect(t('fruit')).toBe('Apples')
expect(t('ns1:fruit')).toBe('Apples')
expect(t('ns2:fruit')).toBe('Oranges')

// Spanish
await i18n.changeLanguage('es')
expect(t('fruit')).toBe('Manzanas')
expect(t('ns1:fruit')).toBe('Manzanas')
expect(t('ns2:fruit')).toBe('Naranjas')
})
5 changes: 0 additions & 5 deletions test/index.spec.ts

This file was deleted.

28 changes: 28 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,15 @@ __metadata:
languageName: node
linkType: hard

"@babel/runtime@npm:^7.12.0":
version: 7.13.10
resolution: "@babel/runtime@npm:7.13.10"
dependencies:
regenerator-runtime: ^0.13.4
checksum: 22014226b96a8c8e8d4e8bcdb011f317d1b32881aef424a669dc6ceaee14993d3609172967853cbf9c25c724c25145d45885b6c9df56ba241c12820776607f1f
languageName: node
linkType: hard

"@babel/template@npm:^7.12.13, @babel/template@npm:^7.3.3":
version: 7.12.13
resolution: "@babel/template@npm:7.12.13"
Expand Down Expand Up @@ -4142,14 +4151,26 @@ fsevents@^2.1.2:
"@typescript-eslint/eslint-plugin": ^4.19.0
"@typescript-eslint/parser": ^4.19.0
eslint: ^7.22.0
i18next: ^20.1.0
jest: ^26.6.3
prettier: ^2.2.1
semantic-release: ^17.4.2
ts-jest: ^26.5.4
typescript: ^4.2.3
peerDependencies:
i18next: ">=20"
languageName: unknown
linkType: soft

"i18next@npm:^20.1.0":
version: 20.1.0
resolution: "i18next@npm:20.1.0"
dependencies:
"@babel/runtime": ^7.12.0
checksum: fb95e19026c2d3d9376087ddfb33c65ba98c80d3e1fb62b93edf7f6b178a492cd5a54bf139edd515a774ee65a75b0051774fdfcf7630f577c26f7014cad2bb12
languageName: node
linkType: hard

"iconv-lite@npm:0.4.24, iconv-lite@npm:~0.4.13":
version: 0.4.24
resolution: "iconv-lite@npm:0.4.24"
Expand Down Expand Up @@ -7667,6 +7688,13 @@ fsevents@^2.1.2:
languageName: node
linkType: hard

"regenerator-runtime@npm:^0.13.4":
version: 0.13.8
resolution: "regenerator-runtime@npm:0.13.8"
checksum: 20178f5753f181d59691e5c3b4c59a2769987f75c7ccf325777673b5478acca61a553b10e895585086c222f72f5ee428090acf50320264de4b79f630f7388653
languageName: node
linkType: hard

"regex-not@npm:^1.0.0, regex-not@npm:^1.0.2":
version: 1.0.2
resolution: "regex-not@npm:1.0.2"
Expand Down

0 comments on commit 37eb5c2

Please sign in to comment.