Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(i18n): translation key autocomplete #231

Merged
merged 32 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
17117a5
feat(i18n): translation key autocomplete
adamdehaven Mar 9, 2023
745def2
fix(i18n): interfaces
adamdehaven Mar 9, 2023
276ad83
fix(document-viewer): translation reference
adamdehaven Mar 9, 2023
f2b92ff
fix: add broken type
adamdehaven Mar 9, 2023
db1a6c0
chore: comment
adamdehaven Mar 9, 2023
93211e9
refactor: change any type to record
adamdehaven Mar 9, 2023
5cbb2de
chore: formatting
adamdehaven Mar 9, 2023
e330568
chore: test file
adamdehaven Mar 9, 2023
61cbd08
fix(i18n): allow ending on a partial path
adamdehaven Mar 12, 2023
09aee5f
fix(spec-operations): update badge colors [khcp-5779] (#232)
aanchalm01 Mar 9, 2023
7541c14
chore(release): publish [skip ci]
kongponents-bot Mar 9, 2023
624cf4b
fix(web-component): bump swagger ui kong theme universal pkg (#233)
davidma415 Mar 10, 2023
8f67856
chore(release): publish [skip ci]
kongponents-bot Mar 10, 2023
8f89149
chore(deps): update dependency @kong/kongponents to ^8.34.1 (#234)
renovate[bot] Mar 10, 2023
0ba4276
fix(deps): update dependency swagger-ui to ^4.16.0 (#235)
renovate[bot] Mar 10, 2023
563b14d
chore(release): publish [skip ci]
kongponents-bot Mar 10, 2023
d37480c
fix(swagger-ui-web-component): bump package version (#236)
davidma415 Mar 10, 2023
aad680a
chore(release): publish [skip ci]
kongponents-bot Mar 10, 2023
d809c6c
chore(deps): update dependency @kong/kongponents to ^8.34.2 (#237)
renovate[bot] Mar 10, 2023
4d3743f
chore(release): publish [skip ci]
kongponents-bot Mar 10, 2023
93b6502
chore(deps): update dependency @kong/kongponents to ^8.36.0 (#238)
renovate[bot] Mar 10, 2023
03ed9a4
chore(release): publish [skip ci]
kongponents-bot Mar 10, 2023
62d0b4c
chore(deps): update dependency @kong/kongponents to ^8.37.0 (#239)
renovate[bot] Mar 11, 2023
65f19e5
chore(deps): update dependency cypress to ^12.7.0 (#240)
renovate[bot] Mar 11, 2023
d617f00
chore(release): publish [skip ci]
kongponents-bot Mar 11, 2023
a506685
fix(types): use proper interface
adamdehaven Mar 12, 2023
e95cb6a
chore(i18n): update interface
adamdehaven Mar 12, 2023
42cc3b9
test(i18n): update unit tests
adamdehaven Mar 13, 2023
32148ff
docs(i18n): update examples
adamdehaven Mar 13, 2023
9d7cef4
Merge branch 'main' into feat/i18n-translation-key-autocomplete
adamdehaven Mar 13, 2023
d61ae69
chore: lockfile
adamdehaven Mar 13, 2023
ad42a5e
feat(i18n): add interface
adamdehaven Mar 13, 2023
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
72 changes: 60 additions & 12 deletions packages/core/i18n/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# @kong-ui-public/i18n

- [Purpose](#purpose)
- [TypeScript Support](#typescript-support)
- [Global Registration](#global-registration)
- [Vue Plugin](#vue-plugin)
- [Use in application](#use-in-application)
- [Use in shared component](#use-in-shared-component)
- [Formatting messages](#formatting-messages)
Expand All @@ -19,6 +22,50 @@ This is a wrapper around [FormatJS](https://formatjs.io/docs/intl) that allows c
- format numbers
- format dates

## TypeScript Support

In order to provide full autocompletion for the translation keys, you need to pass the type of the `messages` object to the various init functions, as shown here:

> **Note**: For the `i18n-t` component (`Translation.ts`), the type checking and auto-completion for the `keypath` prop are still a work-in-progress due to [the issue described here](https://github.com/Kong/public-ui-components/pull/231).
>

### Global Registration

```ts
// main.ts
import { createI18n, Translation } from '@kong-ui-public/i18n'
import english from './locales/en.json'

const i18n = createI18n<typeof english>('en-us', english, true)
app.use(Translation, { i18n })

// composables/index.ts
import { useI18n } from '@kong-ui-public/i18n'
import english from './locales/en.json'

export default {
// Be sure to provide the interface when exporting
useI18n: useI18n<typeof english>,
}

// Component.vue
import composables from './composables'
const i18n = composables.useI18n()
```

### Vue Plugin

```ts
import { Translation, createI18n } from '@kong-ui-public/i18n'
import english from './locales/en.json'

const i18n = createI18n<typeof english>('en-us', english, true)
const app = createApp(App)
app.use(Translation, { i18n })

app.mount('#app')
```

## Use in application

When used in application we only need to instantiate `Intl` object once, during application hydration, and then we can use it anywhere in the app via `useI18n` composable.
Expand All @@ -32,7 +79,7 @@ import english from './locales/en.json'
...

// this will create Application instance of Intl object
createI18n('en-us', english, true)
createI18n<typeof english>('en-us', english, true)
```

And then, anywhere in application code where `i18n` is needed:
Expand All @@ -44,11 +91,11 @@ And then, anywhere in application code where `i18n` is needed:

<script setup lang="ts">
import { useI18n } from '@kong-ui-public/i18n'
const i18n = useI18n()
import english from './locales/en.json'
const i18n = useI18n<typeof english>()
</script>
```


## Use in shared component

When used in [shared component](https://github.com/Kong/shared-ui-components/pull/118/files#diff-f296a0775f650cac9ecd4b5d7ccef015ca73587be833b40eeae1e396426b0f4fR45) (outside of application code), we need `Intl` that is local to that component, so that it is using component's own localization strings:
Expand All @@ -66,7 +113,7 @@ import english from '../locales/en.json'

// this will instantiate `Intl` local to this component, using component's english messages.
// TODO: load and pass messages based on language passed from consuming application
const i18n = createI18n(props.locale || 'en-us', english)
const i18n = createI18n<typeof english>(props.locale || 'en-us', english)
</script>
```

Expand Down Expand Up @@ -103,7 +150,8 @@ const i18n = createI18n(props.locale || 'en-us', english)

<script setup lang="ts">
import { useI18n } from '@kong-ui-public/i18n'
const i18n = useI18n()
import english from './locales/en.json'
const i18n = useI18n<typeof english>()
</script>
```

Expand Down Expand Up @@ -151,8 +199,9 @@ In `khcp-ui` there are many places where instead of using Intl function we are a

<script setup lang="ts">
import { useI18n } from '@kong-ui-public/i18n'
import english from './locales/en.json'

const i18n = useI18n()
const i18n = useI18n<typeof english>()
const helpText = i18n.source.components.docUploadCard
</script>
```
Expand Down Expand Up @@ -185,7 +234,7 @@ import english from './locales/en.json'
...

//this will create Application instance of Intl object
const i18n = createI18n('en-us', english, true)
const i18n = createI18n<typeof english>('en-us', english, true)
// this will register <i18n-t> component
app.use(Translation, { i18n })
```
Expand Down Expand Up @@ -249,7 +298,6 @@ And then, anywhere in application code where `i18n` is needed

In some cases we do not have access to the Vue `app` and cannot relay on registered i18nT plugin. Working on standalone components in `public-ui-components` is of those cases. And for this your component will look like:


```html
<template>
<i18n-t
Expand All @@ -263,8 +311,8 @@ In some cases we do not have access to the Vue `app` and cannot relay on registe
import { createI18n, i18nTComponent } from '@kong-ui-public/i18n'
import english from './locales/en.json'

const i18n = createI18n('en-us', english)
const i18nT = i18nTComponent(i18n)
const i18n = createI18n<typeof english>('en-us', english)
const i18nT = i18nTComponent<typeof english>(i18n)

</script>
```
Expand All @@ -289,10 +337,10 @@ Or in old `defineComponent` way
import english from './locales/en.json'

export default defineComponent({
components: { i18nT: i18nTComponent() },
components: { i18nT: i18nTComponent<typeof english>() },
setup() {
return {
i18n: createI18n('en-us', english)
i18n: createI18n<typeof english>('en-us', english)
}
}
})
Expand Down
6 changes: 3 additions & 3 deletions packages/core/i18n/sandbox/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ import { useI18n, createI18n, i18nTComponent } from '../src'
import english from './locales/en.json'

// this is grabbing i18n from global
const i18n = useI18n()
const i18n = useI18n<typeof english>()

// this creates local i18n and component
const i18nLocal = createI18n<typeof english>('en-us', english)
const i18nNoPlugin = i18nTComponent(i18nLocal)
const i18nLocal = createI18n('en-us', english)
const i18nNoPlugin = i18nTComponent<typeof english>(i18nLocal)
</script>
2 changes: 1 addition & 1 deletion packages/core/i18n/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import App from './App.vue'

import { Translation, createI18n } from '../src/'
import english from './locales/en.json'
const i18n = createI18n('en-us', english, true)
const i18n = createI18n<typeof english>('en-us', english, true)
const app = createApp(App)
app.use(Translation, { i18n })

Expand Down
8 changes: 7 additions & 1 deletion packages/core/i18n/sandbox/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@
"ok": "O-k-key",
"named": "{greeting}, my name is {name}. And I am {occupation}. I want {amount}",
"default": "See the news at {0}. There is a lot there."
},
"custom_dogs": {
"breeds": {
"husky": "Husky",
"beagle": "Beagle"
}
}
}
}
4 changes: 2 additions & 2 deletions packages/core/i18n/src/Translation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ const english = {
},
}

const i18n = createI18n('en-us', english, true)
const i18n = createI18n<typeof english>('en-us', english, true)

describe('TranslationComponent', () => {
it('should render', () => {
const wrapper = mount({
components: { I18nT: i18nTComponent(i18n) },
components: { I18nT: i18nTComponent<typeof english>(i18n) },

setup: () => {
return {
Expand Down
16 changes: 10 additions & 6 deletions packages/core/i18n/src/Translation.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { defineComponent, h } from 'vue'
import type { VNodeChild, App, PropType } from 'vue'
import type { IntlShapeEx } from './types'
import type {
IntlShapeEx,
// PathToDotNotation, // Enable import to debug `keypath` interface
} from './types'

export const i18nTComponent = (i18n: IntlShapeEx | null = null) => defineComponent({
export const i18nTComponent = <MessageSource extends Record<string, any>>(i18n: IntlShapeEx<MessageSource> | null = null) => defineComponent({
name: 'I18nT',
props: {
i18n: {
type: Object as PropType<IntlShapeEx>,
type: Object as PropType<IntlShapeEx<MessageSource>>,
default: null,
},
keypath: {
type: String,
// type: String as unknown as PropType<PathToDotNotation<MessageSource, string>>, // This breaks the type interface, enable to debug
required: true,
},
tag: {
Expand Down Expand Up @@ -39,7 +43,7 @@ export const i18nTComponent = (i18n: IntlShapeEx | null = null) => defineCompone

return (): VNodeChild => {
const keys = Object.keys(slots).filter(key => key !== '_')
const sourceString = (i18n || props.i18n).messages[props.keypath].toString()
const sourceString = (i18n || props.i18n).messages[(props.keypath as string)].toString()

let hArray: Array<any> = deconstructString(sourceString)

Expand All @@ -66,8 +70,8 @@ export const i18nTComponent = (i18n: IntlShapeEx | null = null) => defineCompone

// Export Vue plugin
export default {
install(app: App, options: { i18n: IntlShapeEx }) {
install<MessageSource extends Record<string, any>>(app: App, options: { i18n: IntlShapeEx<MessageSource> }) {
const { i18n } = options
app.component('I18nT', i18nTComponent(i18n))
app.component('I18nT', i18nTComponent<MessageSource>(i18n))
},
}
45 changes: 24 additions & 21 deletions packages/core/i18n/src/i18n.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,49 +20,50 @@ const english = {
describe('i18n', () => {
describe('createI18n & useI18n', () => {
it('Should create local instance of Intl', () => {
const localI18n = createI18n('en-us', english)
const globalI18n = useI18n()
const localI18n = createI18n<typeof english>('en-us', english)
const globalI18n = useI18n<typeof english>()
expect(globalI18n).toBeUndefined()
expect(localI18n?.t).toBeTypeOf('function')
expect(localI18n?.messages['global.ok']).toEqual('Okay')
expect(localI18n?.source.global).toEqual({ ok: 'Okay' })
})

it('Should create global instance of Intl', () => {
createI18n('en-us', english, true)
const i18n = useI18n()
createI18n<typeof english>('en-us', english, true)
const i18n = useI18n<typeof english>()

expect(i18n?.t).toBeTypeOf('function')
expect(i18n?.messages['global.ok']).toEqual('Okay')
expect(i18n?.source.global).toEqual({ ok: 'Okay' })
})

it('Local and global instances should be different', () => {
createI18n('en-us', { global: { ok: 'not Okay' } }, true)
const globalI18n = useI18n()
const globalInstance = { global: { ok: 'not Okay' } }
createI18n<typeof globalInstance>('en-us', globalInstance, true)
const globalI18n = useI18n<typeof globalInstance>()

const localI18n = createI18n('en-us', english)
const localI18n = createI18n<typeof english>('en-us', english)

expect(globalI18n?.messages['global.ok']).not.toEqual(localI18n?.messages['global.ok'])
})
})

describe('function `t`', () => {
beforeAll(() => {
createI18n('en-us', english, true)
createI18n<typeof english>('en-us', english, true)
})
it('should format simple message', () => {
const { t } = useI18n()
const { t } = useI18n<typeof english>()
expect(t('global.ok')).toEqual('Okay')
})

it('should format message with parameters', () => {
const { t } = useI18n()
const { t } = useI18n<typeof english>()
expect(t('test.withParams', { dayPart: 'Morning', name: 'i18n' })).toEqual('Good Morning, My name is i18n!')
})

it('should format message with pluralization', () => {
const { t } = useI18n()
const { t } = useI18n<typeof english>()
expect(t('test.withPluralization', { count: 0 })).toEqual('There are 0 versions available')
expect(t('test.withPluralization', { count: 1 })).toEqual('There are 1 version available')
expect(t('test.withPluralization', { count: 11 })).toEqual('There are 11 versions available')
Expand All @@ -71,35 +72,37 @@ describe('i18n', () => {

describe('function `te`', () => {
beforeAll(() => {
createI18n('en-us', english, true)
createI18n<typeof english>('en-us', english, true)
})

it('should recognize exiting key', () => {
const { te } = useI18n()
const { te } = useI18n<typeof english>()
expect(te('global.ok')).toBeTruthy()
})

it('should recognize non-exiting key', () => {
const { te } = useI18n()
const { te } = useI18n<typeof english>()
// @ts-expect-error
expect(te('global.not.ok')).toBeFalsy()
})
})

describe('function `tm`', () => {
beforeAll(() => {
createI18n('en-us', english, true)
createI18n<typeof english>('en-us', english, true)
})

it('should return array for exiting key', () => {
const { tm } = useI18n()
const { tm } = useI18n<typeof english>()
expect(tm('array.disabled')).toEqual([
'You cannot update configurations for services',
'You cannot accept new developer portal applications',
])
})

it('should return empty array for non-existing key', () => {
const { tm } = useI18n()
const { tm } = useI18n<typeof english>()
// @ts-expect-error
expect(tm('global.not.ok')).toEqual([])
})
})
Expand All @@ -108,11 +111,11 @@ describe('i18n', () => {

describe('formatNumber', () => {
beforeAll(() => {
createI18n('en-us', english, true)
createI18n<typeof english>('en-us', english, true)
})

it('should format currency', () => {
const { formatNumber } = useI18n()
const { formatNumber } = useI18n<typeof english>()

const formattedNumber = formatNumber(1000, { style: 'currency', currency: 'USD' })
expect(formattedNumber).toEqual('$1,000.00')
Expand All @@ -121,10 +124,10 @@ describe('i18n', () => {

describe('formatDate', () => {
beforeAll(() => {
createI18n('en-us', english, true)
createI18n<typeof english>('en-us', english, true)
})
it('should format date', () => {
const { formatDate } = useI18n()
const { formatDate } = useI18n<typeof english>()

const formattedDate = formatDate(Date.parse('12/12/2012'), {
day: 'numeric',
Expand Down
Loading