Kaynağa Gözat

feat(i18n): enhance API response parsing in src/API/i18n/i18nActions.ts

- Update I18nMessages interface to use flat string structure
- Add parseI18nMessages function with robust JSON parsing
- Implement proper request headers for text/javascript content type
- Add comprehensive error handling and data validation
- Support both string and object response formats
- Add gzip compression and UTF-8 encoding support
- Fix TypeScript type safety by using unknown instead of any

Resolves #54
sw 2 hafta önce
ebeveyn
işleme
329b56fdbc
2 değiştirilmiş dosya ile 203 ekleme ve 6 silme
  1. 128 0
      src/API/i18n/i18nActions.test.ts
  2. 75 6
      src/API/i18n/i18nActions.ts

+ 128 - 0
src/API/i18n/i18nActions.test.ts

@@ -0,0 +1,128 @@
+import { fetchI18nMessages } from './i18nActions';
+
+// 模拟 axios 实例
+jest.mock('../interceptor', () => ({
+  get: jest.fn(),
+}));
+
+import axiosInstance from '../interceptor';
+
+describe('i18nActions', () => {
+  const mockAxiosGet = axiosInstance.get as jest.MockedFunction<
+    typeof axiosInstance.get
+  >;
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  describe('fetchI18nMessages', () => {
+    it('should parse object data correctly', async () => {
+      const mockData = {
+        greeting: 'Hello, world!',
+        name: 'John Doe',
+        patient: 'Patient Management',
+        register: 'Register',
+        tasklist: 'Task List',
+        historylist: 'History List',
+        archivelist: 'Archive List',
+        bin: 'Recycle Bin',
+        outputlist: 'Transfer List',
+        exam: 'Examination',
+        examlist: 'Examination List',
+        process: 'Process',
+        print: 'Print',
+        printlist: 'Print List',
+        worklist: 'Task List',
+        'worklist.operationPanel': 'Operation Panel',
+        'register.basicInfoPanel': 'Basic Information Form Area',
+        'register.protocolListPanel': 'Protocol Selection List Area',
+      };
+
+      mockAxiosGet.mockResolvedValue({ data: mockData });
+
+      const result = await fetchI18nMessages('en');
+
+      expect(result).toEqual(mockData);
+      expect(mockAxiosGet).toHaveBeenCalledWith('/pub/trans/en/en.js');
+    });
+
+    it('should parse JSON string data correctly', async () => {
+      const mockData = {
+        greeting: 'Hello, world!',
+        name: 'John Doe',
+        patient: 'Patient Management',
+      };
+      const mockJsonString = JSON.stringify(mockData);
+
+      mockAxiosGet.mockResolvedValue({ data: mockJsonString });
+
+      const result = await fetchI18nMessages('en');
+
+      expect(result).toEqual(mockData);
+    });
+
+    it('should handle non-string values by converting them', async () => {
+      const mockData = {
+        greeting: 'Hello, world!',
+        count: 123,
+        isActive: true,
+        nullValue: null,
+        undefinedValue: undefined,
+      };
+
+      mockAxiosGet.mockResolvedValue({ data: mockData });
+
+      const result = await fetchI18nMessages('en');
+
+      expect(result).toEqual({
+        greeting: 'Hello, world!',
+        count: '123',
+        isActive: 'true',
+        // null 和 undefined 值应该被跳过
+      });
+    });
+
+    it('should throw error for invalid JSON string', async () => {
+      const invalidJsonString = '{ invalid json }';
+
+      mockAxiosGet.mockResolvedValue({ data: invalidJsonString });
+
+      await expect(fetchI18nMessages('en')).rejects.toThrow(
+        'Invalid i18n data: failed to parse JSON string'
+      );
+    });
+
+    it('should throw error for non-object data', async () => {
+      mockAxiosGet.mockResolvedValue({ data: 'not an object' });
+
+      await expect(fetchI18nMessages('en')).rejects.toThrow(
+        'Invalid i18n data: failed to parse JSON string'
+      );
+    });
+
+    it('should handle API errors', async () => {
+      mockAxiosGet.mockRejectedValue(new Error('Network error'));
+
+      await expect(fetchI18nMessages('en')).rejects.toThrow(
+        'Failed to load i18n messages for locale: en'
+      );
+    });
+
+    it('should skip invalid keys', async () => {
+      const mockData = {
+        validKey: 'Valid Value',
+        '': 'Empty key should be skipped',
+        '   ': 'Whitespace key should be skipped',
+      };
+
+      mockAxiosGet.mockResolvedValue({ data: mockData });
+
+      const result = await fetchI18nMessages('en');
+
+      expect(result).toEqual({
+        validKey: 'Valid Value',
+      });
+    });
+  });
+});

+ 75 - 6
src/API/i18n/i18nActions.ts

@@ -1,12 +1,81 @@
 import axiosInstance from '../interceptor';
 
-export interface I18nMessages {
-  [key: string]: string | I18nMessages;
+export type I18nMessages = Record<string, string>;
+
+/**
+ * 解析和验证 i18n 消息数据
+ * @param rawData API 返回的原始数据(可能是字符串或对象)
+ * @returns 标准化的 i18n 消息对象
+ */
+function parseI18nMessages(rawData: unknown): I18nMessages {
+  let data = rawData;
+
+  // 如果是字符串,先解析为对象
+  if (typeof rawData === 'string') {
+    try {
+      data = JSON.parse(rawData.replace(/'/g, '"'));
+    } catch (error) {
+      throw new Error(
+        `'Invalid i18n data: failed to parse JSON string' ${error}`
+      );
+    }
+  }
+
+  if (!data || typeof data !== 'object') {
+    throw new Error('Invalid i18n data: expected an object');
+  }
+
+  const messages: I18nMessages = {};
+
+  // 遍历数据,验证并转换为标准格式
+  for (const [key, value] of Object.entries(data)) {
+    // 确保键是字符串且不为空
+    if (typeof key !== 'string' || key.trim() === '') {
+      console.warn(`Invalid i18n key: ${key}, skipping...`);
+      continue;
+    }
+
+    // 确保值是字符串
+    if (typeof value === 'string') {
+      messages[key] = value;
+    } else if (value !== null && value !== undefined) {
+      // 如果值不是字符串,尝试转换为字符串
+      console.warn(
+        `Non-string value for key "${key}": ${value}, converting to string`
+      );
+      messages[key] = String(value);
+    } else {
+      console.warn(`Null or undefined value for key "${key}", skipping...`);
+    }
+  }
+
+  return messages;
 }
 
 export async function fetchI18nMessages(locale: string): Promise<I18nMessages> {
-  const response = await axiosInstance.get(`/pub/trans/${locale}/${locale}.js`);
-
-  // 返回的是标准JSON格式,直接使用
-  return response.data;
+  try {
+    const response = await axiosInstance.get(
+      `/pub/trans/${locale}/${locale}.js`,
+      {
+        headers: {
+          Accept: 'text/javascript, application/javascript, application/json',
+          'Accept-Encoding': 'gzip, deflate, br',
+          'Accept-Charset': 'utf-8',
+        },
+        responseType: 'json', // 告诉 axios 将响应解析为 JSON
+      }
+    );
+    console.debug(
+      `Fetched i18n messages for locale "${locale} ,${typeof response.data}":`,
+      response.data
+    );
+    // 解析和验证返回的数据
+    return parseI18nMessages(response.data);
+  } catch (error) {
+    console.error(
+      `Failed to fetch i18n messages for locale "${locale}":`,
+      error
+    );
+    throw new Error(`Failed to load i18n messages for locale: ${locale}`);
+  }
 }