芯图相册国际化技术实现总结

58 阅读11分钟

芯图相册国际化技术实现总结

照片按内容,城市,颜色智能分类.png

📋 概述

本文档全面总结了芯图相册(XinTu Album)的国际化(i18n)技术实现方案,涵盖用户界面、动态内容、原生代码和跨平台一致性等各个方面。

技术栈

  • i18n 库: i18next + react-i18next
  • 支持语言: 中文(zh)、英文(en)
  • 存储方案: 语言设置存储在 ImageStorageService 中,统一管理应用配置
  • 平台支持: PC端(Electron)和移动端(React Native Android)

🏗️ 基本实现方案

1.1 核心架构

1.1.1 i18n 初始化配置

文件位置: src/i18n/index.js

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import zh from './locales/zh/common.json';
import en from './locales/en/common.json';

const resources = {
  zh: { common: zh },
  en: { common: en },
};

i18n
  .use(initReactI18next)
  .init({
    resources,
    lng: getSavedLanguageSync(),
    fallbackLng: 'zh',
    defaultNS: 'common',
    interpolation: {
      escapeValue: false, // React已经转义了
    },
    compatibilityJSON: 'v3', // 兼容React Native
  });

关键特性

  • 同步初始化:使用 getSavedLanguageSync() 同步读取语言设置,确保应用启动时语言正确
  • 异步加载:应用启动后通过 loadSavedLanguage() 异步加载保存的语言设置,支持 react-native-localize
  • 语言持久化:语言设置存储在 ImageStorageServiceapp_language 字段中
1.1.2 语言检测机制

系统语言检测

  1. Web/Desktop 环境

    const detectSystemLanguage = () => {
      if (typeof navigator !== 'undefined' && navigator.language) {
        const systemLang = navigator.language.toLowerCase();
        if (systemLang.startsWith('zh')) return 'zh';
        if (systemLang.startsWith('en')) return 'en';
      }
      return 'zh'; // 默认中文
    };
    
  2. React Native 环境

    const detectSystemLanguageAsync = async () => {
      if (isReactNative) {
        const localizeModule = await import('react-native-localize');
        const locales = localizeModule.getLocales();
        if (locales && locales.length > 0) {
          const systemLang = locales[0].languageCode.toLowerCase();
          if (systemLang === 'zh') return 'zh';
          if (systemLang === 'en') return 'en';
        }
      }
      return 'zh';
    };
    

语言设置管理

  • 读取getCurrentLanguageAsync() - 从 ImageStorageService 读取保存的语言设置
  • 保存changeLanguage(lng) - 保存语言设置到 ImageStorageService 并切换 i18n 语言
  • 验证:切换语言后会验证保存是否成功,确保数据一致性

1.2 翻译资源组织

文件结构

src/i18n/
├── index.js                    # i18n 配置和工具函数
└── locales/
    ├── zh/
    │   └── common.json         # 中文翻译资源
    └── en/
        └── common.json         # 英文翻译资源

命名空间

  • common: 通用翻译命名空间(默认)
  • 所有翻译键都使用 common 命名空间,通过 useTranslation('common') 使用

翻译键命名规范

  • app.*: 应用基础信息(如 app.name
  • home.*: 首页相关翻译(如 home.ready, home.scanning
  • category.*: 分类相关翻译(如 category.noImages
  • imagePreview.*: 图片预览相关翻译(如 imagePreview.detectionResult
  • settings.*: 设置页面相关翻译(如 settings.memberActivated
  • common.*: 通用翻译(如 common.success, common.cancel

🎨 用户可见界面信息的国际化

2.1 界面基础文本国际化

2.1.1 React 组件中的使用

标准用法

import { useTranslation } from 'react-i18next';

const MyComponent = () => {
  const { t, i18n } = useTranslation('common');
  
  return (
    <Text>{t('home.ready')}</Text>
  );
};

关键 API

  • t(key, options): 翻译函数,根据当前语言返回对应的翻译文本
  • i18n.language: 获取当前语言代码('zh' 或 'en')
  • i18n.t(key, { lng: 'en' }): 指定语言进行翻译(用于动态切换)
2.1.2 常见界面元素翻译
翻译键中文英文使用位置
app.name芯图相册Xintu Album应用标题
home.ready图片分类应用已就绪Image classification app is ready首页状态
home.scanning扫描中...Scanning...扫描状态
home.scanComplete扫描完成Scan complete扫描完成提示
home.smartScan智能扫描Smart Scan扫描按钮
category.noImages暂无图片No images空状态提示
common.success成功Success通用成功提示
common.cancel取消Cancel取消按钮

英文界面效果展示

照片按内容,城市,颜色智能分类.png

PC端英文界面:按内容、城市、颜色智能分类

2.2 弹窗(Alert)国际化

2.2.1 标准弹窗实现

基础用法

import { useTranslation } from 'react-i18next';
import { Alert } from 'react-native';

const MyComponent = () => {
  const { t, i18n } = useTranslation('common');
  
  const showAlert = () => {
    Alert.alert(
      t('common.success'),
      t('settings.memberActivated')
    );
  };
};
2.2.2 动态语言弹窗(重要)

问题场景:在异步回调(如 setInterval)中,t 函数可能使用的是闭包中的旧语言值。

解决方案:使用 i18n.t() 并显式指定语言参数:

// ❌ 错误示例:可能使用旧的语言值
Alert.alert(t('common.success'), t('settings.memberActivated'));

// ✅ 正确示例:使用当前语言
const currentLang = i18n.language || 'zh';
Alert.alert(
  i18n.t('common.success', { lng: currentLang }),
  i18n.t('settings.memberActivated', { lng: currentLang })
);

实现位置

  • src/screens/mobile/SettingsScreen.mobile.js
  • src/screens/desktop/SettingsScreen.desktop.js

关键代码

// 会员激活弹窗
const currentLang = i18n.language || 'zh';
Alert.alert(
  i18n.t('common.success', { lng: currentLang }),
  i18n.t('settings.memberActivated', { lng: currentLang })
);

🌍 城市信息的国际化

3.1 实现策略

核心原则:在扫描时根据用户语言配置保存对应语言的城市名称,而不是在显示时翻译。

3.1.1 数据来源
  1. 远程API(优先):

    • 返回字段:name(英文)、name_zh(中文)、admin1(省份英文)、admin1_zh(省份中文)
    • API: https://api.bigdatacloud.net/data/reverse-geocode-client
  2. 本地数据(降级):

    • 仅包含中文城市名称
    • 文件:src/data/cities.json
3.1.2 JavaScript 实现

文件位置: src/services/CityLocationService.js

关键方法

async findNearestCityAsync(latitude, longitude, maxDistance = 200, useRemoteApi = true, language = 'zh') {
  // 1. 检查缓存(包含语言信息)
  const cacheKey = this.getCacheKey(latitude, longitude, language);
  
  // 2. 优先使用远程API(传递语言参数)
  if (useRemoteApi) {
    nearestCity = await this.findNearestCityRemote(latitude, longitude, language);
  }
  
  // 3. 如果远程API失败,回退到本地查询(本地数据只有中文)
  if (!nearestCity) {
    nearestCity = this.findNearestCity(latitude, longitude, maxDistance);
  }
  
  // 4. 缓存结果(使用包含语言的缓存键)
  return nearestCity;
}

远程API调用

async findNearestCityRemote(latitude, longitude, language = 'zh') {
  const url = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${latitude}&longitude=${longitude}&localityLanguage=${language}`;
  const response = await fetch(url);
  const data = await response.json();
  
  // 根据语言选择城市名称
  const cityName = language === 'en' 
    ? (data.locality || data.principalSubdivision)  // 英文:优先使用 locality
    : (data.localityInfo?.administrative?.[0]?.name || data.locality);  // 中文:优先使用中文名
  
  return {
    name: cityName,
    province: language === 'en' ? data.principalSubdivision : data.localityInfo?.administrative?.[1]?.name,
    country: language === 'en' ? 'China' : '中国',
    distance: null
  };
}

使用位置: src/services/GalleryScannerService.js

// 获取当前语言设置
const currentLanguage = await getCurrentLanguageAsync();

// 查找最近的城市信息(传递语言参数)
const nearestCity = await cityLocationService.findNearestCityAsync(
  latitude,
  longitude,
  200,
  useRemoteApi,
  currentLanguage  // 传递当前语言设置
);

if (nearestCity) {
  locationInfo.city = nearestCity.name;  // 直接使用对应语言的城市名称
  locationInfo.province = nearestCity.province;
  locationInfo.country = nearestCity.country || '中国';
}
3.1.3 Android 原生实现

文件位置: android/app/src/main/java/com/imageclassifier/v2/GalleryScanService.java

关键代码(第839-866行):

String cityName;
String provinceName;
if ("en".equals(language)) {
    // 英文:优先使用英文名,如果没有则使用中文名
    cityName = mainCity.optString("name", mainCity.optString("name_zh", ""));
    // 省份:优先使用admin1(省份英文名),如果没有则尝试admin1_zh,最后fallback到城市名称
    provinceName = mainCity.optString("admin1", 
        mainCity.optString("admin1_zh", 
            mainCity.optString("name_zh", mainCity.optString("name", ""))));
} else {
    // 中文(默认):优先使用中文名,如果没有则使用英文名
    cityName = mainCity.optString("name_zh", mainCity.optString("name", ""));
    // 省份:优先使用admin1_zh(省份中文名),如果没有则尝试admin1,最后fallback到城市名称
    provinceName = mainCity.optString("admin1_zh", 
        mainCity.optString("admin1", 
            mainCity.optString("name_zh", mainCity.optString("name", ""))));
    // 标准化城市名称:移除"市"后缀(仅中文)
    if (cityName.endsWith("市")) {
        cityName = cityName.substring(0, cityName.length() - 1);
    }
}

Map<String, String> location = new HashMap<>();
location.put("city", cityName);
location.put("country", "en".equals(language) ? "China" : "中国"); // 根据语言设置国家名称
location.put("province", provinceName);

语言参数来源

  • Android 原生代码从 ImageStorageService 读取 app_language 设置
  • 在扫描时传递语言参数给城市查询方法

3.2 显示逻辑

直接显示:城市名称在扫描时已根据用户语言配置保存,显示时直接使用数据库中存储的值,无需额外翻译。

注意事项

  • 已存储的数据不会自动更新:如果用户切换语言,已扫描的图片的城市名称不会自动更新,需要重新扫描
  • 降级处理:如果远程API失败,降级到本地查询时只能保存中文名称(本地数据只有中文)
  • 缓存机制:使用包含语言的缓存键,确保不同语言的结果分别缓存

📂 内容类别信息的国际化

4.1 实现策略

核心原则:从配置文件中读取中英文名称,动态选择显示,而不是存储在 i18n JSON 文件中。

4.1.1 数据来源

配置文件: public/initialSettings.json

{
  "categoryNameMap": {
    "social_activities": {
      "chinese": "社交活动",
      "english": "Social Activities"
    },
    "pets": {
      "chinese": "宠物萌照",
      "english": "Pet Photos"
    },
    "single_person": {
      "chinese": "单人照片",
      "english": "Single Person"
    },
    "foods": {
      "chinese": "美食记录",
      "english": "Food Records"
    },
    "travel_scenery": {
      "chinese": "旅行风景",
      "english": "Travel Scenery"
    },
    "screenshot": {
      "chinese": "手机截图",
      "english": "Mobile Screenshots"
    },
    "other": {
      "chinese": "其它",
      "english": "Other Images"
    },
    "idcard": {
      "chinese": "证件照",
      "english": "ID Card"
    },
    "NA": {
      "chinese": "待分类",
      "english": "to be classified"
    },
    "qrcode": {
      "chinese": "二维码",
      "english": "QR Code"
    }
  }
}
4.1.2 JavaScript 实现

文件位置: src/services/ConfigService.js

关键方法

getCategoryDisplayName(categoryId, language = 'chinese') {
  const categoryByKey = this.getCategoryByKey(categoryId);
  if (categoryByKey) {
    return categoryByKey[language] || categoryByKey.chinese || categoryByKey.english || categoryId;
  }
  
  const category = this.getCategoryById(categoryId);
  if (category) {
    return category[language] || category.chinese || category.english || categoryId;
  }
  
  return categoryId;
}

使用位置: src/screens/desktop/HomeScreen.desktop.js

const categories = configService.getAllCategoriesWithUI().map(category => {
  // 根据当前语言动态选择分类名称
  const currentLang = i18n.language || 'zh';
  const categoryName = currentLang === 'en' 
    ? (category.english || category.chinese)  // 英文:优先使用英文,如果没有则使用中文
    : (category.chinese || category.english);  // 中文:优先使用中文,如果没有则使用英文
  
  return {
    id: category.id,
    name: categoryName,
    icon: '📷',
    color: '#607D8B'
  };
});

图片预览页面: src/screens/desktop/ImagePreviewScreen.desktop.js

const getCategoryInfo = (categoryId) => {
  const category = configService.getCategoryByKey(categoryId);
  if (!category) {
    return {
      name: i18n.t('imagePreview.unknownCategoryName'),
      icon: '📷',
      color: '#607D8B'
    };
  }
  
  // 根据当前语言设置获取分类名称
  const currentLang = i18n.language || 'zh';
  const language = currentLang === 'en' ? 'english' : 'chinese';
  
  return {
    name: category[language] || category.chinese || category.english || categoryId,
    icon: '📷',
    color: '#607D8B'
  };
};

4.2 设计优势

  1. 单一数据源:分类名称只在一个地方维护(initialSettings.json),避免重复
  2. 自动回退:如果首选语言不存在,自动使用另一种语言
  3. 灵活性:支持动态添加新分类,只需更新配置文件
  4. 性能优化:不需要在 i18n JSON 中维护大量分类名称

分类信息国际化效果展示

PC端英文分类

PC端英文界面:按内容、城市、颜色智能分类

移动端英文分类-内容

移动端英文界面:按内容分类

移动端英文分类-城市

移动端英文界面:按城市分类

移动端英文分类-颜色

移动端英文界面:按颜色分类


🤖 智能检测信息的国际化

5.1 扫描进度消息

5.1.1 实现位置

JavaScript 版本: src/services/GalleryScannerService.js Android 原生版本: src/services/GalleryScannerService.android.js

5.1.2 翻译键定义
翻译键中文英文
home.scanProgress.initScanning初始化扫描: 准备扫描环境Initializing scan: Preparing scan environment
home.scanProgress.scanningFiles扫描文件: {{processed}}/{{total}}Scanning files: {{processed}}/{{total}}
home.scanProgress.categoryQuery查询分类: {{processed}}/{{total}}Querying categories: {{processed}}/{{total}}
home.scanProgress.smartRecognitionStart开始智能识别 {{count}} 张图片Starting smart recognition for {{count}} images
home.scanProgress.smartRecognition智能识别: {{processed}}/{{total}}Smart recognition: {{processed}}/{{total}}
home.scanProgress.localRecognitionStart开始本地识别 {{count}} 张图片Starting local recognition for {{count}} images
home.scanProgress.localRecognition本地识别: {{processed}}/{{total}}Local recognition: {{processed}}/{{total}}
home.scanProgress.locationEnrichmentStart开始位置信息丰富 {{count}} 张图片Starting location enrichment for {{count}} images
home.scanProgress.locationEnrichment位置信息丰富: {{processed}}/{{total}}Location enrichment: {{processed}}/{{total}}
home.scanProgress.similarityDetectionStart开始相似度检测Starting similarity detection
home.scanProgress.similarityDetectionProgress相似度检测: {{processed}}/{{total}} ({{groups}} 组)Similarity detection: {{processed}}/{{total}} ({{groups}} groups)
home.scanProgress.removingFiles移除文件: {{count}}Removing files: {{count}}
home.scanProgress.completed扫描完成Scan completed
5.1.3 实现示例

JavaScript 版本

async processProgressData(rawProgress) {
  const { stage, filesProcessed, filesFound } = rawProgress;
  let simpleMessage = '';
  
  switch (stage) {
    case 'init_scanning':
      simpleMessage = i18n.t('home.scanProgress.initScanning');
      break;
    
    case 'scanning_files':
      simpleMessage = i18n.t('home.scanProgress.scanningFiles', {
        processed: filesProcessed || 0,
        total: filesFound || 0
      });
      break;
    
    case 'remote_inference':
      simpleMessage = i18n.t('home.scanProgress.smartRecognition', {
        processed: filesProcessed || 0,
        total: filesFound || 0
      });
      break;
    
    case 'similarity_detection':
      const groupsCount = this.imagesClassified || 0;
      simpleMessage = i18n.t('home.scanProgress.similarityDetectionProgress', {
        processed: filesProcessed,
        total: filesFound,
        groups: groupsCount
      });
      break;
    
    case 'completed':
      simpleMessage = i18n.t('home.scanProgress.completed');
      break;
  }
  
  return { simpleMessage, ... };
}

Android 原生版本(类似实现,但支持更多阶段):

case 'remote_inference':
  if (filesFound > 0 && filesProcessed === 0) {
    simpleMessage = i18n.t('home.scanProgress.smartRecognitionStart', { count: filesFound });
  } else {
    simpleMessage = i18n.t('home.scanProgress.smartRecognition', {
      processed: filesProcessed || 0,
      total: filesFound || 0
    });
  }
  break;

扫描进度消息国际化效果展示

移动端扫描进度

移动端英文界面:照片扫描进展显示


5.2 检测结果标签

5.2.1 翻译键定义
翻译键中文英文
imagePreview.detectionResult检测结果Detection Result
imagePreview.classificationComplete分类完成Classification Complete
imagePreview.objects个对象objects
imagePreview.idCardDetection身份证检测ID Card Detection
imagePreview.idCardFront身份证正面ID Card Front
imagePreview.idCardBack身份证背面ID Card Back
imagePreview.generalDetection通用物体检测General Object Detection
imagePreview.uncategorizedCategory未分类Uncategorized
imagePreview.unknownCategoryName未知分类Unknown Category
5.2.2 使用示例

文件位置: src/screens/desktop/ImagePreviewScreen.desktop.js

{/* 检测结果显示 */}
<View style={styles.infoRow}>
  <Text style={styles.infoLabel}>
    🔍 {t('imagePreview.detectionResult')}:
  </Text>
  <Text style={styles.infoValue}>
    {currentImage.message && currentImage.message !== i18n.t('imagePreview.classificationComplete') 
      ? currentImage.message 
      : `${((currentImage.idCardDetections?.length || 0) + (currentImage.generalDetections?.length || 0))}${t('imagePreview.objects')}`
    }
  </Text>
</View>

{/* 身份证检测结果 */}
{currentImage.idCardDetections && currentImage.idCardDetections.length > 0 && (
  <View style={styles.detectionSection}>
    <Text style={styles.detectionTitle}>
      🆔 {t('imagePreview.idCardDetection')}:
    </Text>
    {currentImage.idCardDetections.map((detection, index) => (
      <View key={index} style={styles.detectionItem}>
        <Text style={styles.detectionText}>
          {detection.class === 'id_card_front' 
            ? t('imagePreview.idCardFront') 
            : t('imagePreview.idCardBack')
          }
          ({(detection.confidence * 100).toFixed(1)}%)
        </Text>
      </View>
    ))}
  </View>
)}

检测结果国际化效果展示

照片预览以及信息查看.png

PC端英文界面:照片预览以及信息查看

单照片预览.jpg 移动端英文界面:单照片预览

查看单照片的详细信息.jpg 移动端英文界面:查看单照片的详细信息

照片相似组检测.jpg 移动端英文界面:照片相似组检测


💻 原生代码和JS代码的国际化

6.1 JavaScript 代码国际化

6.1.1 标准实现

导入和使用

import { useTranslation } from 'react-i18next';
import i18n, { getCurrentLanguageAsync } from '../../i18n';

const MyComponent = () => {
  const { t, i18n } = useTranslation('common');
  
  // 使用翻译函数
  const message = t('home.ready');
  
  // 获取当前语言
  const currentLang = i18n.language || 'zh';
  
  // 指定语言翻译
  const englishMessage = i18n.t('home.ready', { lng: 'en' });
};
6.1.2 异步语言获取

场景:在非 React 组件中(如 Service 类)需要获取当前语言。

实现

import { getCurrentLanguageAsync } from '../i18n';

class MyService {
  async doSomething() {
    // 异步获取当前语言设置
    const currentLanguage = await getCurrentLanguageAsync();
    
    // 根据语言执行不同逻辑
    if (currentLanguage === 'en') {
      // 英文逻辑
    } else {
      // 中文逻辑
    }
  }
}

6.2 Android 原生代码国际化

6.2.1 语言参数传递

实现位置: android/app/src/main/java/com/imageclassifier/v2/GalleryScanService.java

关键步骤

  1. 读取语言设置

    // 从 ImageStorageService 读取语言设置
    String language = getLanguageFromSettings(); // 返回 "zh" 或 "en"
    
  2. 传递语言参数

    // 在城市查询时传递语言参数
    Map<String, String> location = getCityLocation(latitude, longitude, language);
    
  3. 根据语言返回对应文本

    if ("en".equals(language)) {
        cityName = mainCity.optString("name", mainCity.optString("name_zh", ""));
        country = "China";
    } else {
        cityName = mainCity.optString("name_zh", mainCity.optString("name", ""));
        country = "中国";
    }
    
6.2.2 原生代码限制

当前实现

  • 城市名称:原生代码根据语言设置返回对应语言的城市名称
  • 国家名称:根据语言返回 "China" 或 "中国"
  • 其他文本:原生代码中的其他文本(如错误消息)目前未国际化

未来改进建议

  • 考虑使用 Android 的 Resources 系统进行国际化
  • 或者将更多文本逻辑移到 JavaScript 层处理

🔄 PC端和移动端国际化的一致性

7.1 共享配置

7.1.1 统一的 i18n 配置

文件位置: src/i18n/index.js

关键特性

  • 跨平台兼容:使用 compatibilityJSON: 'v3' 确保 React Native 兼容性
  • 统一语言资源:PC端和移动端使用相同的翻译文件
  • 统一语言设置:语言设置存储在 ImageStorageService 中,PC端使用 IndexedDB,移动端使用 SQLite
7.1.2 统一的语言设置管理

存储方案

  • PC端: ImageStorageServiceIndexedDBsettings.app_language
  • 移动端: ImageStorageServiceSQLitesettings.app_language

读取和保存

// 统一的 API,自动适配平台
const storageService = await getImageStorageService();
const settings = await storageService.getSettings();
const language = settings.app_language || 'zh';

// 保存语言设置
await storageService.saveSettings({ ...settings, app_language: 'en' });

7.2 组件实现一致性

7.2.1 相同的翻译键

PC端和移动端使用相同的翻译键

  • src/screens/desktop/HomeScreen.desktop.js
  • src/screens/mobile/HomeScreen.mobile.js

两者都使用相同的翻译键,如:

  • home.ready
  • home.scanning
  • category.noImages
  • imagePreview.detectionResult
7.2.2 相同的实现模式

分类名称处理

// PC端和移动端使用相同的逻辑
const currentLang = i18n.language || 'zh';
const categoryName = currentLang === 'en' 
  ? (category.english || category.chinese)
  : (category.chinese || category.english);

颜色名称翻译

// PC端和移动端都使用相同的工具函数
import { getColorNameTranslation } from '../../i18n';

const displayColorName = getColorNameTranslation(color, i18n.language);

7.3 数据一致性

7.3.1 城市信息

PC端和移动端

  • ✅ 使用相同的 CityLocationService
  • ✅ 使用相同的远程API和本地数据
  • ✅ 根据相同的语言设置保存城市名称
7.3.2 分类信息

PC端和移动端

  • ✅ 使用相同的 ConfigService
  • ✅ 读取相同的 initialSettings.json
  • ✅ 使用相同的分类名称映射逻辑

🔧 动态内容的处理方案

8.1 服务器返回内容的国际化

8.1.1 颜色名称翻译

问题:服务器API返回的颜色名称可能是中文或英文,需要根据用户语言设置翻译。

解决方案:使用颜色名称映射表。

实现位置: src/i18n/index.js

const COLOR_NAME_MAP = {
  // 中文 -> 英文
  '橙色': 'Orange',
  '蓝色': 'Blue',
  '红色': 'Red',
  // ... 更多映射
  // 英文 -> 中文
  'Orange': '橙色',
  'Blue': '蓝色',
  'Red': '红色',
  // ... 更多映射
};

export const getColorNameTranslation = (colorName, language = null) => {
  const targetLang = language || getCurrentLanguage();
  const normalizedColorName = colorName.trim();
  
  if (targetLang === 'zh') {
    // 目标语言是中文
    // 如果已经是中文,直接返回
    if (COLOR_NAME_MAP[normalizedColorName] && 
        ['橙色', '蓝色', '红色', ...].includes(normalizedColorName)) {
      return normalizedColorName;
    }
    // 尝试从英文映射到中文
    const chineseName = COLOR_NAME_MAP[normalizedColorName];
    if (chineseName) return chineseName;
  } else {
    // 目标语言是英文
    // 如果已经是英文,直接返回(首字母大写)
    const englishNames = ['Orange', 'Blue', 'Red', ...];
    if (englishNames.includes(normalizedColorName)) {
      return normalizedColorName.charAt(0).toUpperCase() + normalizedColorName.slice(1).toLowerCase();
    }
    // 尝试从中文映射到英文
    const englishName = COLOR_NAME_MAP[normalizedColorName];
    if (englishName) return englishName;
  }
  
  // 如果找不到映射,返回原始值
  return colorName;
};

使用位置: src/screens/desktop/HomeScreen.desktop.js

import { getColorNameTranslation } from '../../i18n';

const ColorCard = ({ color }) => {
  const displayColorName = useMemo(() => {
    return getColorNameTranslation(color, i18n.language);
  }, [color, i18n.language]);
  
  return <Text>{displayColorName}</Text>;
};
8.1.2 方向名称翻译

类似实现:使用方向名称映射表。

实现位置: src/i18n/index.js

const ORIENTATION_NAME_MAP = {
  '横屏': 'Landscape',
  '竖屏': 'Portrait',
  '正方形': 'Square',
  '全景': 'Panorama',
  // ... 反向映射
};

export const getOrientationNameTranslation = (orientationName, language = null) => {
  // 类似颜色名称翻译的实现
  // ...
};

8.2 动态消息的国际化

8.2.1 带参数的翻译

实现:使用 i18n 的插值功能。

// 翻译键定义
{
  "home.scanProgress.scanningFiles": "扫描文件: {{processed}}/{{total}}",
  "home.scanProgress.similarityDetectionProgress": "相似度检测: {{processed}}/{{total}} ({{groups}} 组)"
}

// 使用
i18n.t('home.scanProgress.scanningFiles', {
  processed: 10,
  total: 100
});
// 输出: "扫描文件: 10/100"
8.2.2 AI 描述消息

问题:AI 返回的描述消息可能是中文或英文,需要根据用户语言设置处理。

当前实现

  • AI 描述消息直接显示,不做翻译(因为 AI 本身会根据上下文生成对应语言的描述)
  • 如果 AI 返回的消息不符合用户语言设置,可能需要改进 AI 提示词

未来改进建议

  • 在 AI 请求时传递语言参数,确保返回对应语言的描述
  • 或者在后端进行翻译

📝 最佳实践总结

9.1 翻译键命名规范

  1. 使用有意义的命名空间home.*, category.*, imagePreview.*
  2. 使用描述性的键名home.scanComplete 而不是 home.msg1
  3. 保持一致性:PC端和移动端使用相同的翻译键

9.2 动态内容处理

  1. 服务器返回内容:使用映射表进行翻译(如颜色名称、方向名称)
  2. 配置数据:从配置文件读取中英文名称,动态选择(如分类名称)
  3. 扫描时保存:根据用户语言设置保存对应语言的数据(如城市名称)

9.3 语言设置管理

  1. 统一存储:使用 ImageStorageService 统一管理语言设置
  2. 异步读取:在 Service 类中使用 getCurrentLanguageAsync() 异步读取
  3. 验证保存:切换语言后验证保存是否成功

9.4 弹窗和异步回调

  1. 使用当前语言:在异步回调中使用 i18n.t(key, { lng: currentLang }) 而不是闭包中的 t 函数
  2. 显式指定语言:确保使用最新的语言值

🚀 未来改进建议

10.1 已实现功能

  • ✅ 基础界面文本国际化
  • ✅ 弹窗国际化(包括动态语言切换)
  • ✅ 城市信息国际化(扫描时保存)
  • ✅ 分类信息国际化(配置文件读取)
  • ✅ 智能检测信息国际化(扫描进度、检测结果)
  • ✅ 颜色名称翻译(映射表)
  • ✅ 方向名称翻译(映射表)
  • ✅ PC端和移动端一致性

10.2 待改进功能

  1. 城市名称映射表

    • 考虑创建城市名称映射表,支持已存储数据的国际化显示
    • 避免重新扫描才能看到对应语言的城市名称
  2. Android 原生代码国际化

    • 使用 Android Resources 系统进行更全面的国际化
    • 或者将更多文本逻辑移到 JavaScript 层
  3. AI 描述消息

    • 在 AI 请求时传递语言参数
    • 确保返回对应语言的描述
  4. 日期格式化

    • 使用 i18n 的日期格式化功能
    • 统一日期显示格式
  5. 数字格式化

    • 根据语言设置格式化数字(如千位分隔符)

📚 相关文件清单

核心文件

  • src/i18n/index.js - i18n 配置和工具函数
  • src/i18n/locales/zh/common.json - 中文翻译资源
  • src/i18n/locales/en/common.json - 英文翻译资源

服务文件

  • src/services/ImageStorageService.js - 语言设置存储
  • src/services/CityLocationService.js - 城市信息国际化
  • src/services/ConfigService.js - 分类信息国际化
  • src/services/GalleryScannerService.js - 扫描进度消息国际化

原生代码

  • android/app/src/main/java/com/imageclassifier/v2/GalleryScanService.java - Android 原生城市信息国际化

配置文件

  • public/initialSettings.json - 分类名称配置

📖 参考资料


文档版本: 1.0.0
最后更新: 2025-01-20
维护者: 芯图相册开发团队