Skip to content

Commit

Permalink
provide I18nT to simplify element translation
Browse files Browse the repository at this point in the history
  • Loading branch information
nighca committed Oct 30, 2024
1 parent ee999cc commit cf75ef6
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 46 deletions.
3 changes: 2 additions & 1 deletion spx-gui/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ module.exports = {
// These rules will match components in both kebab-case and CamelCase
'router-view',
'router-link',
'v-.*' // for Vue Konva components
'v-.*', // for Vue Konva components
'I18nT'
]
}
],
Expand Down
25 changes: 9 additions & 16 deletions spx-gui/src/components/project/ProjectPublishedModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,6 @@ const emit = defineEmits<{
const projectPageRoute = computed(() => getProjectPageRoute(props.project.owner!, props.project.name!))
const projectPageLink = computed(() => `${location.origin}${projectPageRoute.value}`)
// TODO: support vnode as i18n message to simplify such case
const preLinkText = {
en: 'Visit ',
zh: '访问'
}
const linkText = {
en: 'project page',
zh: '项目主页'
}
const postLinkText = {
en: ', or copy the link below to share the project with others.',
zh: ',或者复制下方链接将项目分享给其他人。'
}
const handleCopy = useMessageHandle(
() => navigator.clipboard.writeText(projectPageLink.value),
{ en: 'Failed to copy link to clipboard', zh: '分享链接复制到剪贴板失败' },
Expand All @@ -47,8 +33,15 @@ const handleCopy = useMessageHandle(
@update:visible="emit('cancelled')"
>
<div class="desc">
{{ $t(preLinkText) }}<UILink target="_blank" :href="projectPageRoute">{{ $t(linkText) }}</UILink
>{{ $t(postLinkText) }}
<I18nT>
<template #en>
Visit <UILink target="_blank" :href="projectPageRoute">project page</UILink>, or copy the link below to share
the project with others.
</template>
<template #zh>
访问<UILink target="_blank" :href="projectPageRoute">项目主页</UILink>,或者复制下方链接将项目分享给其他人。
</template>
</I18nT>
</div>
<div class="link-wrapper">
<UITextInput :value="projectPageLink" :readonly="true" @focus="$event.target.select()" />
Expand Down
35 changes: 11 additions & 24 deletions spx-gui/src/pages/community/search.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
<template>
<CommunityHeader>
<template v-if="titleForResult != null">
<!-- TODO: support vnode as i18n message to simplify such case -->
{{ $t(titleForResult.prefix) }}<span class="keyword">{{ keyword }}</span
>{{ $t(titleForResult.suffix) }}
</template>
<I18nT v-if="queryRet.data.value != null">
<template #en>
Found {{ queryRet.data.value?.total }} projects for "<span class="keyword">{{ keyword }}</span
>"
</template>
<template #zh>
找到 {{ queryRet.data.value?.total }} 个关于“<span class="keyword">{{ keyword }}</span
>”的项目
</template>
</I18nT>
<template v-else>
{{ $t(titleForNoResult) }}
{{ $t({ en: 'Search projects', zh: '搜索项目' }) }}
</template>
<template #options>
<label>
Expand Down Expand Up @@ -121,24 +126,6 @@ const queryRet = useQuery(() => listProject(listParams.value), {
en: 'Failed to search projects',
zh: '搜索项目失败'
})
const titleForResult = computed(() => {
if (queryRet.data.value == null) return null
return {
prefix: {
en: `Found ${queryRet.data.value?.total} projects for "`,
zh: `找到 ${queryRet.data.value?.total} 个关于“`
},
suffix: {
en: '"',
zh: '”的项目'
}
}
})
const titleForNoResult = computed(() => ({
en: 'Search projects',
zh: '搜索项目'
}))
</script>

<style lang="scss" scoped>
Expand Down
19 changes: 16 additions & 3 deletions spx-gui/src/utils/i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ i18n.setLang('zh')
### Do translation in template of SFC

```vue
<button>
{{$t({ en: 'Sign in', zh: '登录' })}}
</button>
<template>
<button>
{{ $t({ en: 'Sign in', zh: '登录' }) }}
</button>
</template>
```

### Do translation in setup script
Expand All @@ -39,6 +41,17 @@ const { t } = useI18n()
const signoutText = t({ en: 'Sign out', zh: '登出' })
```

### Translate complex elements in template of SFC

```vue
<template>
<I18nT>
<template #en>Please <a :href="link">sign in</a>.</template>
<template #zh>请<a :href="link">登录</a>。</template>
</I18nT>
</template>
```

### Locale Message Functions

Locale-message-functions are functions that return locale message. It is useful when extra information is needed when constructing locale messages. For example:
Expand Down
38 changes: 36 additions & 2 deletions spx-gui/src/utils/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@
* @desc Simple i18n tool for vue
*/

import { inject, type App, type InjectionKey, type ObjectPlugin, ref, type Ref } from 'vue'
import {
inject,
type App,
type InjectionKey,
type ObjectPlugin,
ref,
type Ref,
type SetupContext,
type SlotsType
} from 'vue'

export type Lang = 'en' | 'zh'

Expand All @@ -26,6 +35,9 @@ declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$t: TranslateFn
}
interface GlobalComponents {
I18nT: typeof T
}
}

const injectKey: InjectionKey<I18n> = Symbol('i18n')
Expand Down Expand Up @@ -57,16 +69,38 @@ export class I18n implements ObjectPlugin<[]> {
install(app: App<unknown>) {
app.provide(injectKey, this)
app.config.globalProperties.$t = this.t
app.component('I18nT', T as any)
}
}

// NOTE: `T` (also known as `I18nT`) is a suitable choice for translating complex content within templates in Single File Component (SFC).
// It relies on a straightforward mechanism and integrates well with SFC features such as scoped styles.
// If we decide to externalize locale messages from components into separate locale data in the future, we need to:
// 1. Provide support for locale messages of type `VNode`.
// 2. Adjust component code to properly handle potential issues with SFC features like scoped styles.

/**
* Translate component. Use slot with lang as key to provide translated content. e.g.,
* ```html
* <I18nT>
* <template #en>English</template>
* <template #zh>中文</template>
* </I18nT>
* ```
*/
function T(_props: unknown, { slots }: SetupContext<unknown, SlotsType<Record<Lang, any>>>) {
const i18n = inject(injectKey)
if (i18n == null) throw new Error('i18n not installed')
return slots[i18n.lang.value]?.()
}

export function createI18n(config: I18nConfig) {
return new I18n(config)
}

export function useI18n() {
const i18n = inject(injectKey)
if (i18n == null) throw new Error('i18n not used')
if (i18n == null) throw new Error('i18n not installed')
return i18n
}

Expand Down
2 changes: 2 additions & 0 deletions spx-gui/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ export function memoizeAsync<T extends (...args: any) => Promise<unknown>>(
}) as T
}

// TODO: we may move these `humanizeX` functions to i18n module as exposed helpers

/** Convert time string to human-friendly format, e.g., "3 days ago" */
export function humanizeTime(time: string): LocaleMessage {
const t = dayjs(time)
Expand Down

0 comments on commit cf75ef6

Please sign in to comment.