Skip to content

Commit

Permalink
remove FunctionLocaleMessage
Browse files Browse the repository at this point in the history
  • Loading branch information
nighca committed Apr 1, 2024
1 parent 82290db commit cdb7c9e
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 81 deletions.
54 changes: 28 additions & 26 deletions spx-gui/src/components/project/ProjectCreate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
<NInput v-model:value="formValue.name" />
</NFormItem>
<NFormItem>
<NButton @click="handleCancel">
<NButton type="tertiary" @click="handleCancel">
{{ _t({ en: 'Cancel', zh: '取消' }) }}
</NButton>
<NButton @click="handleSubmit">
<NButton type="primary" @click="handleSubmit">
{{ _t({ en: 'Create', zh: '创建' }) }}
</NButton>
</NFormItem>
Expand All @@ -17,9 +17,9 @@
<script setup lang="ts">
import { ref } from 'vue'
import { NForm, NFormItem, NInput, NButton, type FormInst } from 'naive-ui'
import { type ProjectData, getProject, addProject, IsPublic } from '@/apis/project'
import { type ProjectData, getProject, addProject as rawAddProject, IsPublic } from '@/apis/project'
import { useFormRules, type ValidationResult } from '@/utils/form'
import { useMessageHandle, cancel } from '@/utils/exception'
import { useMessageHandle } from '@/utils/exception'
import { useUserStore } from '@/stores/user'
import { ApiException, ApiExceptionCode } from '@/apis/common/exception'
Expand All @@ -44,36 +44,38 @@ function handleCancel() {
emit('cancelled')
}
const handleSubmit = useMessageHandle(
async () => {
if (formRef.value == null) cancel()
const errs = await formRef.value.validate().then( // TODO: extract such logic to utils/form
() => [],
e => {
if (Array.isArray(e)) return e
throw e
}
)
if (errs.length > 0) cancel()
const projectData = await addProject({
name: formValue.value.name,
isPublic: IsPublic.personal,
files: {}
})
emit('created', projectData)
},
{ en: 'Failed to create project', zh: '项目创建失败' },
{ en: 'Project created', zh: '创建成功' }
const addProject = useMessageHandle(
rawAddProject,
{ en: 'Failed to create project', zh: '创建失败' },
project => ({ en: `Project ${project.name} created`, zh: `项目 ${project.name} 创建成功` })
)
async function handleSubmit() {
if (formRef.value == null) return
const errs = await formRef.value.validate().then( // TODO: extract such logic to utils/form
() => [],
e => {
if (Array.isArray(e)) return e
throw e
}
)
if (errs.length > 0) return
const projectData = await addProject({
name: formValue.value.name,
isPublic: IsPublic.personal,
files: {}
})
emit('created', projectData)
}
async function validateName(name: string): Promise<ValidationResult> {
name = name.trim()
if (name === '') return { en: 'The project name must not be blank', zh: '项目名不可为空' }
if (!/^[\w-]+$/.test(name)) return {
en: 'The project name can only contain ASCII letters, digits, and the characters -, and _.',
zh: '项目名仅可包含字母、数字以及 - & _'
en: 'The project name can only contain ASCII letters, digits, and the characters - and _',
zh: '项目名仅可包含字母、数字、符号 - _'
}
if (name.length > 100) return {
Expand Down
1 change: 0 additions & 1 deletion spx-gui/src/components/top-menu/TopMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,3 @@ const toggleLanguage = useToggleLanguage()
</script>

<style lang="scss" scoped></style>
../project
49 changes: 26 additions & 23 deletions spx-gui/src/utils/exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { useMessage } from 'naive-ui'
import { useI18n } from './i18n'
import type { LocaleMessage, FunctionLocaleMessage } from './i18n'
import type { LocaleMessage } from './i18n'

/**
* Exceptions are like errors, while slightly different:
Expand All @@ -28,6 +28,11 @@ export class DefaultException extends Exception {
}
}

/**
* Cancelled is a special exception, it stands for a "cancel operation" because of user ineraction.
* Like other exceptions, it breaks normal flows, while it is supposed to be ignored by all user-feedback components,
* so the user will not be notified of cancelled exceptions.
*/
export class Cancelled extends Exception {
name = 'Cancelled'
userMessage = null
Expand All @@ -36,43 +41,41 @@ export class Cancelled extends Exception {
}
}

export function cancel(): never {
throw new Cancelled()
}

const failedMessage: FunctionLocaleMessage<[summary: string, reason: string | null]> = {
en: (summary, reason) => (reason ? `${summary} (${reason})` : summary),
zh: (summary, reason) => (reason ? `${summary}${reason})` : summary)
}
const failedMessage = (summary: string, reason: string | null) => ({
en: reason ? `${summary} (${reason})` : summary,
zh: reason ? `${summary}${reason})` : summary
})

export function useMessageHandle<F extends () => Promise<unknown>>(
action: F,
export function useMessageHandle<Args extends any[], Ret>(
action: (...args: Args) => Promise<Ret>,
failureSummaryMessage: LocaleMessage,
successMessage?: LocaleMessage
): F {
successMessage?: LocaleMessage | ((ret: Ret) => LocaleMessage)
): (...args: Args) => Promise<Ret> {
const m = useMessage()
const { t } = useI18n()

return (() => {
return action().then(
return ((...args: Args) => {
return action(...args).then(
(ret) => {
if (successMessage != null) {
m.success(() => t(successMessage))
const successText = t(typeof successMessage === 'function' ? successMessage(ret) : successMessage)
m.success(() => successText)
}
return ret
},
(e) => {
if (e instanceof Cancelled) return
let reasonMessage: LocaleMessage | null = null
if (e instanceof Exception && e.userMessage != null) {
reasonMessage = e.userMessage
if (!(e instanceof Cancelled)) {
let reasonMessage: LocaleMessage | null = null
if (e instanceof Exception && e.userMessage != null) {
reasonMessage = e.userMessage
}
const result = t(failedMessage(t(failureSummaryMessage), t(reasonMessage)))
m.error(() => result)
}
const result = t(failedMessage, t(failureSummaryMessage), t(reasonMessage))
m.error(() => result)
throw e
}
)
}) as F
})
}

// TODO: helpers for in-place feedback
Expand Down
26 changes: 7 additions & 19 deletions spx-gui/src/utils/i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,17 @@ const { t } = useI18n()
const signoutText = t({ en: 'Sign out', zh: '登出' })
```

### Function Locale Message
### Locale Message Functions

Function-locale-messages are messages that extra information are needed when translating. For example:
Locale-message-functions are functions that return locale message. It is useful when extra information is needed when constructing locale messages. For example:

```ts
const projectSummaryMessage: FunctionLocaleMessage<[num: number]> = {
en: num => `You have ${num} project${num > 1 ? 's' : ''}`,
zh: num => `你有 ${num} 个项目`
}
const projectSummaryMessage = (num: number) => ({
en: `You have ${num} project${num > 1 ? 's' : ''}`,
zh: `你有 ${num} 个项目`
})

const projectSummary = t(projectSummaryMessage, 3) // "You have 3 projects" / "你有 3 个项目"
const projectSummary = t(projectSummaryMessage(3)) // "You have 3 projects" / "你有 3 个项目"
```

It's like [interpolations](https://vue-i18n.intlify.dev/guide/essentials/syntax.html#interpolations) in vue-i18n, but simpler & more powerful.
Expand All @@ -75,15 +75,3 @@ const helloMessage = {
const resultMessage = mapMessage(helloMessage, hello => hello + ' foo')
console.log(t(resultMessage)) // "Hello foo" / "你好 foo"
```

We can also use `mapMessage` with function-locale-messages:

```ts
const projectSummaryMessage: FunctionLocaleMessage<[num: number]> = {
en: num => `You have ${num} project${num > 1 ? 's' : ''}`,
zh: num => `你有 ${num} 个项目`
}

const resultMessage = mapMessage(projectSummaryMessage, f => f(3))
console.log(t(resultMessage)) // "You have 3 projects" / "你有 3 个项目"
```
14 changes: 2 additions & 12 deletions spx-gui/src/utils/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ export type Translated = string

export type LocaleMessage = Record<Lang, Translated>

export type FunctionLocaleMessage<Args extends any[]> = Record<Lang, (...args: Args) => Translated>

export interface I18nConfig {
/** Initial lang */
lang: Lang
Expand Down Expand Up @@ -43,17 +41,9 @@ export class I18n implements ObjectPlugin<[]> {
/** Translate */
t(message: LocaleMessage): Translated
t(message: LocaleMessage | null): Translated | null
t<Args extends any[]>(message: FunctionLocaleMessage<Args>, ...args: Args): Translated
t<Args extends any[]>(
message: FunctionLocaleMessage<Args> | null,
...args: Args
): Translated | null
t(message: LocaleMessage | FunctionLocaleMessage<unknown[]> | null, ...args: unknown[]) {
t(message: LocaleMessage | null) {
if (message == null) return null
const val = message[this.lang.value]
if (typeof val === 'function') {
return (val as any)(...args)
}
return val
}

Expand All @@ -74,7 +64,7 @@ export function useI18n() {
return i18n
}

export function mapMessage<M extends LocaleMessage | FunctionLocaleMessage<any[]>, T>(
export function mapMessage<M extends LocaleMessage, T>(
message: M,
process: (value: M[keyof M], lang: Lang) => T
): Record<Lang, T> {
Expand Down

0 comments on commit cdb7c9e

Please sign in to comment.