Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ lib/
**/coverage/
**/.coverage/
**/jest-cache/
**/.jest-cache/
**/__data__/**
2 changes: 1 addition & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
**/coverage
**/mochawesome-report
**/mochawesome-reports
**/jest-cache
**/.jest-cache/
**/.github/
**/.workflow/
.travis/
Expand Down
48 changes: 48 additions & 0 deletions DEV/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { useEffect, useMemo } from 'react'
import I18nContext from './I18nContext'
import I18nResolver from './I18nResolver'
import transform from './transform'
import useObservableState from './useObservableState'


/**
* @typedef Props
* @property {import("@daniloster/i18n/lib/types").I18nState} i18n
*/


/**
* Application dev
* @param {Props} props
*/
export default function App({ i18n }) {
const [language] = useObservableState(i18n.language)
const [t] = useObservableState(i18n.t)
const [status] = useObservableState(i18n.status)

useEffect(() => {
i18n.init()
}, [i18n])
const i18nContextValue = useMemo(() => [i18n, transform], [i18n])

return (
<I18nContext.Provider value={i18nContextValue}>
<div>
<div>
<button type="button" onClick={() => i18n.languageService.set('en').catch(() => null)}>EN</button>
<button type="button" onClick={() => i18n.languageService.set('pt').catch(() => null)}>PT</button>
<button type="button" onClick={() => i18n.languageService.set('es').catch(() => null)}>ES</button>
</div>
<p>{status}</p>
<p>{language}</p>
<p>{t('PageOne.hello')}</p>
<p><I18nResolver path="PageOne.hello" /></p>
<p><I18nResolver path="PageOne.interpolation.text" /></p>
<p><I18nResolver path="PageOne.interpolation.nodes" /></p>
<div>
<I18nResolver path="PageOne.interpolation.simple" modifiers={{ name: 'Guest' }} />
</div>
</div>
</I18nContext.Provider>
)
}
55 changes: 55 additions & 0 deletions DEV/AppForTest.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import I18nContext from './I18nContext'
import I18nResolver from './I18nResolver'
import transform from './transform'
import useObservableState from './useObservableState'

/**
* @typedef Props
* @property {import("@daniloster/i18n/lib/types").I18nState} i18n
*/

/**
* Application test
* @param {Props} props
*/
export default function AppForTest({ i18n }) {
const [language] = useObservableState(i18n.language)
const [t] = useObservableState(i18n.t)
const [status] = useObservableState(i18n.status)
const [asyncError, setAsyncError] = useState(null)
const handleDebounceError = useCallback((e) => {
if (e.debounced) {
return null
}

setAsyncError(e)
}, [])

useEffect(() => {
i18n.init()
}, [i18n])
const i18nContextValue = useMemo(() => [i18n, transform], [i18n])

return (
<I18nContext.Provider value={i18nContextValue}>
<div>
<div>
<button type="button" data-testid="EN" onClick={() => i18n.languageService.set('en').catch(handleDebounceError)}>EN</button>
<button type="button" data-testid="PT" onClick={() => i18n.languageService.set('pt').catch(handleDebounceError)}>PT</button>
<button type="button" data-testid="ES" onClick={() => i18n.languageService.set('es').catch(handleDebounceError)}>ES</button>
</div>
{!!asyncError && <p>error: {asyncError.message}</p>}
<p>status: {status}</p>
<p>language: {language}</p>
<p>{t('PageOne.hello')}</p>
<p><I18nResolver path="PageOne.hello" /></p>
<p><I18nResolver path="PageOne.interpolation.text" /></p>
<p><I18nResolver path="PageOne.interpolation.nodes" /></p>
<div>
<I18nResolver path="PageOne.interpolation.simple" modifiers={{ name: 'Guest' }} />
</div>
</div>
</I18nContext.Provider>
)
}
3 changes: 3 additions & 0 deletions DEV/I18nContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createContext } from 'react'

export default createContext([null, null])
23 changes: 23 additions & 0 deletions DEV/I18nResolver.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react'
import useI18nResolver from './useI18nResolver'


/**
* @typedef Props
* @property {string} namespace
* @property {string} path
* @property {{ [key: string]: any }} modifiers
*/

/**
*
* @param {Props} props
*/
export default function I18nResolver(props) {
const { namespace, path, modifiers } = props
const t = useI18nResolver({ namespace })

return (
<>{t(path, modifiers)}</>
)
}
10 changes: 10 additions & 0 deletions DEV/assets/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"PageOne": {
"hello": "Hello World",
"interpolation": {
"text": "Hey {name}! It is good to have you here. Thanks {name} for coming.",
"nodes": "Hey {name}! It is good <decorator>to have you here</decorator>. Thanks {name} for coming.",
"simple": "Hey {name} (awesome)!"
}
}
}
10 changes: 10 additions & 0 deletions DEV/assets/locales/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"PageOne": {
"hello": "Hola Mundo",
"interpolation": {
"text": "¡Hola, {name}! Es bueno tenerte aquí. Gracias, {name} por venir.",
"nodes": "¡Hola {name}! Es bueno <decorator> tenerte aquí </decorator>. Gracias {name} por venir.",
"simple": "¡Hola {name} (super fantástico)!"
}
}
}
10 changes: 10 additions & 0 deletions DEV/assets/locales/pt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"PageOne": {
"hello": "Ola Mundo",
"interpolation": {
"text": "Ei, {name}! É bom ter você aqui. Obrigado {name} por ter vindo.",
"nodes": "Ei, {name}! É bom <decorator> tê-lo aqui </decorator>. Obrigado {name} por ter vindo.",
"simple": "Ei {name} (super fantástico)!"
}
}
}
Empty file added DEV/dev.d.ts
Empty file.
24 changes: 24 additions & 0 deletions DEV/factoryI18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import factoryI18n from '@daniloster/i18n'

/**
* @returns {import('@daniloster/i18n/lib/types').I18nState}
*/
export default (props) => factoryI18n({
defaultLanguage: 'en',
languageService: {
get: async () => localStorage.getItem('language'),
set: async (language) => localStorage.setItem('language', language),
},
resourcesService: {
get: async (language) => {
const response = await fetch(`/assets/locales/${language}.json`)
if (response.ok) {
return await response.json()
}

throw new Error(`[ERROR]: ${response.status} | ${response.statusText}`)
},
set: async () => null
},
...props,
})
16 changes: 16 additions & 0 deletions DEV/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import factoryI18n from './factoryI18n'

export default factoryI18n({
defaultLanguage: 'en',
languageService: {
get: async () => localStorage.getItem('language'),
set: async (language) => localStorage.setItem('language', language),
},
resourcesService: {
get: async (language) => {
const response = await fetch(`/assets/locales/${language}.json`)
return await response.json()
},
set: async () => Promise.resolve(null)
},
})
11 changes: 11 additions & 0 deletions DEV/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import '@babel/polyfill'
import React from 'react'
import { render } from 'react-dom'
import App from './App'
import i18n from './i18n'


const root = document.createElement('div')
document.body.appendChild(root)

render(<App i18n={i18n} />, root)
9 changes: 9 additions & 0 deletions DEV/transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function interpolate(resolution, [key, value]) {
return resolution.replace(new RegExp(`{${key}}`, 'g'), value)
}

export default function transform({ template, modifiers }) {
return Object
.entries(modifiers || {})
.reduce(interpolate, template)
}
36 changes: 36 additions & 0 deletions DEV/useI18nResolver.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useCallback, useContext } from 'react'
import I18nContext from './I18nContext'
import useObservableState from "./useObservableState"


/**
* @typedef Props
* @property {string} namespace
*/

/**
*
* @param {Props} props
*/
export default function useI18nResolver(props) {
const { namespace } = props
/** @type {[import('@daniloster/i18n/lib/types').I18nState, import('@daniloster/i18n/lib/types').ResolverTransformer]} */
const [i18n, transformer] = useContext(I18nContext)
const [t] = useObservableState(i18n.t)

return useCallback((path, modifiers) => {
/** @type {import('@daniloster/i18n/lib/types').ResolverTransformer} */
const interpolate = (options) => {
if (transformer) {
return transformer({ ...options, modifiers })
}

return options.template || options.path
}

return t(
namespace ? namespace + '.' + path : path,
transformer ? interpolate : transformer,
)
}, [t, transformer])
}
26 changes: 26 additions & 0 deletions DEV/useObservableState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useEffect, useMemo, useState } from 'react'

/**
* Listen to an observable state converting into react hook
* @param {import('@daniloster/i18n/lib/types').ObservableState<S>} observableState
* @returns {[S, ((value: S) => S) => void]}
* @template S
*/
export default function useObservableState(observableState) {
/** @type {[S, React.Dispatch<React.SetStateAction<S>>]} */
const state = useState(() => observableState.get())
const [value, setValue] = state

useEffect(() => {
const subscription = observableState.subscribe({
next: (newValue) => {
setValue((oldValue) => oldValue !== newValue ? newValue : oldValue)
}
})

return subscription.unsubscribe
}, [observableState])

/** @type {[S, ((value: S) => S) => void]} */
return useMemo(() => [value, observableState.set], [value, observableState.set])
}
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# @daniloster/i18n

![Main workflow](https://github.com/daniloster/i18n/workflows/Main%20workflow/badge.svg)

## Contents

- [Getting Started](https://github.com/daniloster/i18n/blob/master/docs/GETTING_STARTED.md)
- [Contributions](#Contributions)
- [Steps](#Steps)
- [Dev](#Dev)

## IE Polyfills

- [string.prototype.matchall](https://www.npmjs.com/package/string.prototype.matchall)

## Contributions

Feel free to reach out and help improving this library.

### Steps

- Pull the repository `git clone https://github.com/daniloster/i18n.git`
- Creating your contribution
- Add at least one commit with message containing either `[patch]`, `[minor]` or `[major]`
- Add at least one commit with message containing `[release]`, otherwise, it won't be released.
- Do not use `git pull`, go for `git fetch` and `git rebase`
- Add tests as "the changes are intended to be used by dev"

### Dev

- **Testing**: `yarn test` or `yarn test --watch`
- **Web Dev App**: `yarn dev`
3 changes: 3 additions & 0 deletions docs/GETTING_STARTED.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Getting Started

`@daniloster/i18n` provides a simple abstraction layer to manage internationalization.
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ process.env.TZ = 'UTC'
process.env.NODE_ICU_DATA = 'node_modules/full-icu'

module.exports = {
cacheDirectory: './jest-cache',
cacheDirectory: './.jest-cache',
coverageReporters: ['html', 'json', 'lcov', 'text'],
coverageDirectory: '<rootDir>/coverage/',
collectCoverage: true,
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@daniloster/i18n",
"version": "0.0.1",
"version": "0.0.0",
"description": "Library to apply internationalization for node or javascript projects",
"main": "lib/index.js",
"types": "lib/types.d.ts",
Expand Down Expand Up @@ -74,6 +74,7 @@
"mutation-helper": "1.0.0"
},
"resolutions": {
"jsdom": "16.2.2"
"jsdom": "16.2.2",
"lodash": "4.17.19"
}
}
Loading