shigma / schemastery

Type driven schema validator
https://shigma.github.io/schemastery/
MIT License
65 stars 10 forks source link

Feature Request: support i18n description #36

Closed MaikoTan closed 1 year ago

MaikoTan commented 1 year ago

Summary

Hitorically schemastery and schemastery-vue didn't support i18n, you could only get what you written in the .description('whatever') part.

Recently I have met the situation that we should render i18n description with schemastery-vue, so I digged into schemastery's code and found a way to hackily add i18n description outside of schemastery.

Click to Expand The localise function I used is coming from `element-plus`, but I think any function has a shape like `(desc: string) => string` probably work, either. ```ts import { useLocale } from 'element-plus' export function hackSchemaDescription(schema: Schema, id: string) { if (!schema) return const { type, meta } = schema if (meta?.description) { schema.meta.description = t(`components.${id}.${meta.description}`) } switch (type) { case 'dict': case 'bitset': case 'object': Object.entries(schema.dict).forEach(([_, value]) => { hackSchemaDescription(value, id) }) break case 'array': case 'tuple': case 'union': case 'intersect': schema.list?.forEach((value) => { hackSchemaDescription(value, id) }) break } return schema } ```

But I think this feature should be implemented by schemastery itself.

Proposed Workflow

  1. Add a second parameter in Schema's constructor, which could be named option, which has a type like:
interface SchemaOptions {
  transform: {
    description: (desc: string) => string
    // maybe we could add more transformers for other methods?
    // like `link` that we could add prefixes to it.
  }
}
  1. Invoke the option?.transform?.description(value) within the code below (maybe between L235 and L236):

https://github.com/shigma/schemastery/blob/560ae3ce42ac3f7f6a4ff32383a3038b8d3f8f79/packages/core/src/index.ts#L233-L241

Additional Information

As what I using for the localisation is element-plus's built-in useLocale, the composable function could automatically trigger the re-render when user switches the language. But I don't know if others could do the same thing. Why I noticed this was that schemastery is using Object.assign to put the description into the Schema object (or function?), I am not very familiar with this, but I was trapped in this before.

MaikoTan commented 1 year ago

The workflow above not working, which means we should pass the transformers recursively into every single Schema object. Also, since Koishi's schema was read from the backend, a function could not be serialized in this case, which make it difficult to implement.

So a better method is to add transformers on schemastery-vue, I have figured out a method, see if this satisfy you. This approach do not require useLocale or something, theoretically any localization function should work. I could produce a PR if you have no comments on it.

diff is here, or see the text version below.

diff --git a/packages/form/src/group.vue b/packages/form/src/group.vue
index 2390d22..4cfe124 100644
--- a/packages/form/src/group.vue
+++ b/packages/form/src/group.vue
@@ -12,7 +12,7 @@
           <el-input v-model="entries[index][0]"></el-input>
         </template>
         <template #description>
-          <k-markdown :source="schema.inner.meta.description"></k-markdown>
+          <k-markdown :source="getDescription(schema.inner, transformers)"></k-markdown>
         </template>
         <template #menu>
           <el-dropdown-item divided :disabled="!index" command="up">上移</el-dropdown-item>
:...skipping...
diff --git a/packages/form/src/group.vue b/packages/form/src/group.vue
index 2390d22..4cfe124 100644
--- a/packages/form/src/group.vue
+++ b/packages/form/src/group.vue
@@ -12,7 +12,7 @@
           <el-input v-model="entries[index][0]"></el-input>
         </template>
         <template #description>
-          <k-markdown :source="schema.inner.meta.description"></k-markdown>
+          <k-markdown :source="getDescription(schema.inner, transformers)"></k-markdown>
         </template>
         <template #menu>
           <el-dropdown-item divided :disabled="!index" command="up">上移</el-dropdown-item>
@@ -28,7 +28,8 @@
           :schema="{ ...schema.inner, meta: { ...schema.inner.meta, description: '' } }"
           :disabled="disabled"
           :instant="instant"
-          :prefix="schema.type === 'array' ? `${prefix.slice(0, -1)}[${key}].` : prefix + key + '.'">
+          :prefix="schema.type === 'array' ? `${prefix.slice(0, -1)}[${key}].` : prefix + key + '.'"
+          :transformers="transformers">
           <span class="prefix">{{ prefix }}</span>
           <span>{{ key }}</span>
         </k-schema>
@@ -43,6 +44,7 @@
       :disabled="disabled"
       :instant="instant"
       :prefix="schema.type === 'array' ? `${prefix.slice(0, -1)}[${key}].` : prefix + key + '.'"
+      :transformers="transformers"
       @command="handleCommand($event, index)">
       <template #menu>
         <el-dropdown-item divided :disabled="!index" command="up">上移</el-dropdown-item>
@@ -64,7 +66,7 @@
 <script lang="ts" setup>

 import { PropType, ref, watch, WatchStopHandle } from 'vue'
-import { getFallback, isObjectSchema, Schema } from './utils'
+import { getFallback, getDescription, isObjectSchema, Schema, Transformers } from './utils'
 import SchemaItem from './item.vue'

 function handleCommand(action: string, index?: number) {
@@ -99,6 +101,7 @@ const props = defineProps({
   disabled: Boolean,
   instant: Boolean,
   signal: Boolean,
+  transformers: { type: Object as PropType<Transformers>, default: () => ({}) },
 })

 const emit = defineEmits(['update:modelValue', 'update:signal'])
diff --git a/packages/form/src/schema.vue b/packages/form/src/schema.vue
index da63ada..b10b94b 100644
--- a/packages/form/src/schema.vue
+++ b/packages/form/src/schema.vue
@@ -3,14 +3,15 @@
   <template v-else-if="schema.type === 'const' || schema.type === 'never'"></template>

   <template v-else-if="schema.type === 'object'">
-    <h2 class="k-schema-header" v-if="schema.meta.description">{{ schema.meta.description }}</h2>
+    <h2 class="k-schema-header" v-if="schema.meta.description">{{ getDescription(schema, transformers) }}</h2>
     <k-schema v-for="(item, key) in schema.dict" :key="key"
       v-model="config[key]"
       :schema="item"
       :initial="initial?.[key]"
       :instant="instant"
       :disabled="disabled"
-      :prefix="prefix + key + '.'">
+      :prefix="prefix + key + '.'"
+      :transformers="transformers">
       <span class="prefix">{{ prefix }}</span>
       <span>{{ key }}</span>
     </k-schema>
@@ -24,7 +25,8 @@
       :schema="{ ...item, meta: { ...item.meta, ...schema.meta } }"
       :instant="instant"
       :disabled="disabled"
-      :prefix="prefix">
+      :prefix="prefix"
+      :transformers="transformers">
       <slot></slot>
     </k-schema>
   </template>
@@ -45,7 +47,7 @@
     </template>

     <template #description>
-      <k-markdown :source="schema.meta.description"></k-markdown>
+      <k-markdown :source="getDescription(schema, transformers)"></k-markdown>
     </template>

     <template #right>
@@ -55,7 +57,7 @@
             v-for="(item, index) in choices"
             :key="index"
             :value="index"
-            :label="item.meta.description || item.value"
+            :label="getDescription(item, transformers) || item.value"
           ></el-option>
         </el-select>
       </template>
@@ -80,7 +82,7 @@
           v-model="config"
           :disabled="disabled"
           :label="item.value"
-        >{{ item.meta.description || item.value }}</el-radio>
+        >{{ getDescription(item, transformers) || item.value }}</el-radio>
       </li>
     </ul>

@@ -112,7 +114,7 @@
     <!-- top level array / dict -->
     <template v-else>
       <h2 class="k-schema-header">
-        {{ schema.meta.description || '配置列表' }}
+        {{ getDescription(schema, transformers) || '配置列表' }}
         <el-button solid @click="signal = true" :disabled="disabled">添加项</el-button>
       </h2>
       <schema-group v-model:signal="signal"
@@ -130,6 +132,7 @@
       :instant="instant"
       :disabled="disabled"
       :prefix="prefix"
+      :transformers="transformers"
     ></k-schema>
   </template>
 </template>
@@ -138,7 +141,7 @@

 import { watch, ref, computed } from 'vue'
 import type { PropType } from 'vue'
-import { deepEqual, getChoices, getFallback, Schema, validate } from './utils'
+import { deepEqual, getChoices, getDescription, getFallback, Schema, Transformers, validate } from './utils'
 import { clone, isNullable, valueMap } from 'cosmokit'
 import BitCheckbox from './bit.vue'
 import SchemaItem from './item.vue'
@@ -154,6 +157,7 @@ const props = defineProps({
   disabled: Boolean,
   branch: Boolean,
   prefix: { type: String, default: '' },
+  transformers: { type: Object as PropType<Transformers>, default: () => ({}) },
 })

 const emit = defineEmits(['update:modelValue', 'command'])
@@ -196,7 +200,7 @@ watch(() => props.schema, (value) => {
 const selectModel = computed({
   get() {
     if (active.value === props.schema) return
-    return active.value.meta.description || active.value.value
+    return getDescription(active.value, props.transformers) || active.value.value
   },
   set(index) {
     if (active.value === choices.value[index]) return
diff --git a/packages/form/src/utils.ts b/packages/form/src/utils.ts
index 9fb3841..915c2c4 100644
--- a/packages/form/src/utils.ts
+++ b/packages/form/src/utils.ts
@@ -78,3 +78,19 @@ export function hasTitle(schema: Schema, root?: boolean): boolean {
     return false
   }
 }
+
+export type Transformer<I = any, O = I> = (value: I, schema: Schema) => O
+
+export interface Transformers {
+  description: Transformer<string>
+}
+
+export function getDescription(schema: Schema, transformers: Transformers): string | undefined {
+  if (!schema?.meta?.description) return
+
+  const description = schema.meta.description
+  if (transformers?.description) {
+    return transformers.description(description, schema)
+  }
+  return description
+}
shigma commented 1 year ago

已经实现。