Преглед изворни кода

refactor: i18n (#374)

* refactor: i18n

* refactor: i18n

* feat: free-container-plugin support i18n
xiamidaxia пре 7 месеци
родитељ
комит
39734e5a02

+ 3 - 2
apps/demo-free-layout-simple/src/index.css

@@ -1,11 +1,10 @@
 .demo-free-node {
     display: flex;
-    min-width: 300px;
-    min-height: 100px;
     flex-direction: column;
     align-items: flex-start;
     box-sizing: border-box;
     border-radius: 8px;
+    position: relative;
     border: 1px solid var(--light-usage-border-color-border, rgba(28, 31, 35, 0.08));
     background: #fff;
     box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);
@@ -21,6 +20,8 @@
     padding: 4px 12px;
     flex-grow: 1;
     width: 100%;
+    background-color: white;
+    border-radius: 0 0 8px 8px;
 }
 .demo-free-node::before {
     content: '';

+ 10 - 0
apps/demo-free-layout/src/hooks/use-editor-props.tsx

@@ -194,6 +194,16 @@ export function useEditorProps(
       onDispose() {
         console.log('---- Playground Dispose ----');
       },
+      i18n: {
+        locale: navigator.language,
+        languages: {
+          'zh-CN': {
+            'Never Remind': '不再提示',
+            'Hold {{key}} to drag node out': '按住 {{key}} 可以将节点拖出',
+          },
+          'en-US': {},
+        },
+      },
       plugins: () => [
         /**
          * Line render plugin

+ 22 - 0
common/config/rush/pnpm-lock.yaml

@@ -1843,6 +1843,9 @@ importers:
       '@flowgram.ai/utils':
         specifier: workspace:*
         version: link:../utils
+      i18n-js:
+        specifier: ^4.5.1
+        version: 4.5.1
     devDependencies:
       '@flowgram.ai/eslint-config':
         specifier: workspace:*
@@ -2676,6 +2679,9 @@ importers:
       '@flowgram.ai/free-lines-plugin':
         specifier: workspace:*
         version: link:../free-lines-plugin
+      '@flowgram.ai/i18n':
+        specifier: workspace:*
+        version: link:../../common/i18n
       '@flowgram.ai/renderer':
         specifier: workspace:*
         version: link:../../canvas-engine/renderer
@@ -10222,6 +10228,10 @@ packages:
   /big.js@5.2.2:
     resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
 
+  /bignumber.js@9.3.0:
+    resolution: {integrity: sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==}
+    dev: false
+
   /binary-extensions@2.3.0:
     resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
     engines: {node: '>=8'}
@@ -12970,6 +12980,14 @@ packages:
     dependencies:
       ms: 2.1.3
 
+  /i18n-js@4.5.1:
+    resolution: {integrity: sha512-n7jojFj1WC0tztgr0I8jqTXuIlY1xNzXnC3mjKX/YjJhimdM+jXM8vOmn9d3xQFNC6qDHJ4ovhdrGXrRXLIGkA==}
+    dependencies:
+      bignumber.js: 9.3.0
+      lodash: 4.17.21
+      make-plural: 7.4.0
+    dev: false
+
   /iconv-lite@0.4.24:
     resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
     engines: {node: '>=0.10.0'}
@@ -14036,6 +14054,10 @@ packages:
     resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
     dev: false
 
+  /make-plural@7.4.0:
+    resolution: {integrity: sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg==}
+    dev: false
+
   /markdown-extensions@1.1.1:
     resolution: {integrity: sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==}
     engines: {node: '>=0.10.0'}

+ 1 - 1
packages/canvas-engine/renderer/src/flow-renderer-registry.ts

@@ -95,7 +95,7 @@ export class FlowRendererRegistry {
   }
 
   getText(textKey: string) {
-    return I18n.t(textKey, { disableReturnKey: true }) || this.textMap.get(textKey);
+    return I18n.t(textKey, { defaultValue: '' }) || this.textMap.get(textKey);
   }
 
   /**

+ 15 - 9
packages/common/i18n/__tests__/i18n.test.ts

@@ -4,28 +4,27 @@ import { I18n } from '../src';
 
 describe('i18n', () => {
   it('default', () => {
-    expect(I18n.getLocalLanguage()).toBe('en-US');
-    expect(I18n.getLangauges().size).toBe(2);
+    expect(I18n.locale).toBe('en-US');
   });
-  it('setLocalLanguage', () => {
+  it('setLocal', () => {
     let changeTimes = 0;
     let dispose = I18n.onLanguageChange((langId) => {
       changeTimes++;
     });
-    I18n.setLocalLanguage('en-US');
+    I18n.locale = 'en-US';
     expect(changeTimes).toEqual(0);
-    I18n.setLocalLanguage('zh-CN');
+    I18n.locale = 'zh-CN';
     expect(changeTimes).toEqual(1);
     dispose.dispose();
-    I18n.setLocalLanguage('en-US');
+    I18n.locale = 'en-US';
     expect(changeTimes).toEqual(1);
   });
   it('translation', () => {
     expect(I18n.t('Yes')).toEqual('Yes');
-    I18n.setLocalLanguage('zh-CN');
+    I18n.locale = 'zh-CN';
     expect(I18n.t('Yes')).toEqual('是');
     expect(I18n.t('Unknown')).toEqual('Unknown');
-    expect(I18n.t('Unknown', { disableReturnKey: true })).toEqual('');
+    expect(I18n.t('Unknown', { defaultValue: '' })).toEqual('');
     I18n.addLanguage({
       languageId: 'zh-CN',
       contents: {
@@ -33,6 +32,13 @@ describe('i18n', () => {
       },
     });
     expect(I18n.t('Unknown')).toEqual('未知');
-    expect(I18n.t('Unknown', { disableReturnKey: true })).toEqual('未知');
+    expect(I18n.t('Unknown', { defaultValue: '' })).toEqual('未知');
+  });
+  it('missingStrictMode', () => {
+    I18n.locale = 'en-US';
+    I18n.missingStrictMode = true;
+    expect(I18n.t('Unknown')).toEqual('[missing "en-US.Unknown" translation]');
+    I18n.missingStrictMode = false;
+    expect(I18n.t('Unknown')).toEqual('Unknown');
   });
 });

+ 2 - 1
packages/common/i18n/package.json

@@ -25,7 +25,8 @@
     "watch": "npm run build:fast -- --dts-resolve --watch --ignore-watch dist"
   },
   "dependencies": {
-    "@flowgram.ai/utils": "workspace:*"
+    "@flowgram.ai/utils": "workspace:*",
+    "i18n-js": "^4.5.1"
   },
   "devDependencies": {
     "@flowgram.ai/eslint-config": "workspace:*",

+ 56 - 36
packages/common/i18n/src/index.ts

@@ -1,71 +1,91 @@
+import { I18n as I18nStore } from 'i18n-js';
 import { Emitter } from '@flowgram.ai/utils';
 
+type Scope = Readonly<string | string[]>;
+
+interface TranslateOptions {
+  defaultValue?: any;
+  [key: string]: any;
+}
+
 interface I18nLanguage {
   languageId: string;
   languageName?: string;
   localizedLanguageName?: string;
-  contents: Record<string, string>;
+  contents: Record<string, string | string[]>;
 }
 
 import zhCNLanguageDefault from './i18n/zh-CN';
 import enUSLanguageDefault from './i18n/en-US';
 
+function getDefaultLanugage(): string {
+  if (typeof navigator !== 'object') return 'en-US';
+  const defaultLanguage = navigator.language;
+  if (defaultLanguage === 'en' || defaultLanguage === 'en-US') {
+    return 'en-US';
+  }
+  if (defaultLanguage === 'zh' || defaultLanguage === 'zh-CN') {
+    return 'zh-CN';
+  }
+  return defaultLanguage;
+}
 class I18nImpl {
-  private _languages = new Map<string, I18nLanguage>();
-
-  private _localLanguage = 'en-US';
+  public i18n = new I18nStore();
 
   private _onLanguageChangeEmitter = new Emitter<string>();
 
   readonly onLanguageChange = this._onLanguageChangeEmitter.event;
 
   constructor(languages: I18nLanguage[]) {
-    languages.forEach((language) => this.addLanguage(language));
+    this.addLanguages(languages);
+    this.locale = getDefaultLanugage();
+    this.i18n.onChange(() => {
+      this._onLanguageChangeEmitter.fire(this.i18n.locale);
+    });
   }
 
   /**
-   * TODO support replace
+   * missing check
+   */
+  missingStrictMode = false;
+
+  /**
    * @param key
    * @param options
    */
-  t(key: string, options?: { disableReturnKey?: boolean }): string {
-    const contents: Record<string, string> =
-      this._languages.get(this._localLanguage)?.contents || {};
-    if (contents[key]) {
-      return contents[key];
-    }
-    if (options?.disableReturnKey) return '';
-    return key;
+  t(key: Scope, options?: TranslateOptions): string {
+    return this.i18n.t(key, {
+      defaultValue: this.missingStrictMode ? undefined : key,
+      ...options,
+    });
   }
 
-  getLocalLanguage() {
-    return this._localLanguage;
+  get locale(): string {
+    return this.i18n.locale;
   }
 
-  setLocalLanguage(langId: string) {
-    if (langId === this._localLanguage) return;
-    this._localLanguage = langId;
-    this._onLanguageChangeEmitter.fire(langId);
+  set locale(locale: string) {
+    this.i18n.locale = locale;
   }
 
-  getLangauges() {
-    return this._languages;
+  addLanguages(newLanguage: I18nLanguage[]): void {
+    this.i18n.store(
+      newLanguage.reduce(
+        (dict, lang) =>
+          Object.assign(dict, {
+            [lang.languageId]: {
+              languageName: lang.languageName,
+              localizedLanguageName: lang.localizedLanguageName,
+              ...lang.contents,
+            },
+          }),
+        {}
+      )
+    );
   }
 
-  addLanguage(newLanguage: I18nLanguage): void {
-    let oldLanguage = this._languages.get(newLanguage.languageId);
-    if (oldLanguage) {
-      this._languages.set(newLanguage.languageId, {
-        ...oldLanguage,
-        ...newLanguage,
-        contents: {
-          ...oldLanguage.contents,
-          ...newLanguage.contents,
-        },
-      });
-    } else {
-      this._languages.set(newLanguage.languageId, newLanguage);
-    }
+  addLanguage(language: I18nLanguage) {
+    this.addLanguages([language]);
   }
 }
 

+ 1 - 0
packages/plugins/free-container-plugin/package.json

@@ -34,6 +34,7 @@
         "@flowgram.ai/free-layout-core": "workspace:*",
         "@flowgram.ai/renderer": "workspace:*",
         "@flowgram.ai/utils": "workspace:*",
+        "@flowgram.ai/i18n": "workspace:*",
         "inversify": "^6.0.1",
         "reflect-metadata": "~0.2.2",
         "lodash": "^4.17.21"

+ 7 - 3
packages/plugins/free-container-plugin/src/sub-canvas/components/tips/index.tsx

@@ -1,5 +1,7 @@
 import React from 'react';
 
+import { I18n } from '@flowgram.ai/i18n';
+
 import { useControlTips } from './use-control';
 import { SubCanvasTipsStyle } from './style';
 import { isMacOS } from './is-mac-os';
@@ -7,12 +9,14 @@ import { IconClose } from './icon-close';
 
 interface SubCanvasTipsProps {
   tipText?: string | React.ReactNode;
+  neverRemindText?: string | React.ReactNode;
 }
 
-export const SubCanvasTips = ({ tipText }: SubCanvasTipsProps) => {
+export const SubCanvasTips = ({ tipText, neverRemindText }: SubCanvasTipsProps) => {
   const { visible, close, closeForever } = useControlTips();
 
-  const displayContent = tipText || `Hold ${isMacOS ? 'Cmd ⌘' : 'Ctrl'} to drag node out`;
+  const displayContent =
+    tipText || I18n.t('Hold {{key}} to drag node out', { key: isMacOS ? 'Cmd ⌘' : 'Ctrl' });
 
   if (!visible) {
     return null;
@@ -35,7 +39,7 @@ export const SubCanvasTips = ({ tipText }: SubCanvasTipsProps) => {
         </div>
         <div className="actions">
           <p className="close-forever" onClick={closeForever}>
-            Never Remind
+            {neverRemindText || I18n.t('Never Remind')}
           </p>
           <div className="close" onClick={close}>
             <IconClose />

+ 25 - 4
packages/plugins/i18n-plugin/src/create-i18n-plugin.ts

@@ -2,8 +2,20 @@ import { I18n, type I18nLanguage } from '@flowgram.ai/i18n';
 import { definePluginCreator } from '@flowgram.ai/core';
 
 export interface I18nPluginOptions {
+  locale?: string;
+  /**
+   * use `locale` instead
+   * @deprecated
+   */
   localLanguage?: string;
-  languages?: I18nLanguage[];
+  /**
+   * if missingStrictMode is true
+   *  expect(I18n.t('Unknown')).toEqual('[missing "en-US.Unknown" translation]')
+   * else
+   *  expect(I18n.t('Unknown')).toEqual('Unknown')
+   */
+  missingStrictMode?: boolean;
+  languages?: I18nLanguage[] | Record<string, Record<string, any>>;
   onLanguageChange?: (languageId: string) => void;
 }
 /**
@@ -15,10 +27,19 @@ export const createI18nPlugin = definePluginCreator<I18nPluginOptions>({
       ctx.playground.toDispose.push(I18n.onLanguageChange(_opts.onLanguageChange));
     }
     if (_opts.languages) {
-      _opts.languages.forEach((language) => I18n.addLanguage(language));
+      if (Array.isArray(_opts.languages)) {
+        I18n.addLanguages(_opts.languages);
+      } else {
+        I18n.addLanguages(
+          Object.keys(_opts.languages).map((key) => ({
+            languageId: key,
+            contents: (_opts.languages as any)![key],
+          }))
+        );
+      }
     }
-    if (_opts.localLanguage) {
-      I18n.setLocalLanguage(_opts.localLanguage);
+    if (_opts.locale || _opts.localLanguage) {
+      I18n.locale = (_opts.locale || _opts.localLanguage)!;
     }
   },
 });