Эх сурвалжийг харах

refactor(更新)
- 国际化支持CSV导入、热切换、DataBinding
- 蓝牙队列、重连机制优化
- 修复若干文本错误

周文健 2 сар өмнө
parent
commit
56eed844e5
38 өөрчлөгдсөн 1738 нэмэгдсэн , 39 устгасан
  1. 637 0
      app/src/main/assets/i18n/zh-CN.csv
  2. 20 0
      app/src/main/java/com/grkj/iscs/ISCSApplication.kt
  3. 1 1
      app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/AddPointDialog.kt
  4. 1 1
      app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/UpdatePointDialog.kt
  5. 2 6
      app/src/main/java/com/grkj/iscs/features/main/fragment/home/HomeFragment.kt
  6. 1 1
      app/src/main/res/layout-land/fragment_job_execute.xml
  7. 1 1
      app/src/main/res/layout/dialog_add_card.xml
  8. 1 1
      app/src/main/res/layout/dialog_add_point.xml
  9. 1 1
      app/src/main/res/layout/dialog_filter_card.xml
  10. 1 1
      app/src/main/res/layout/dialog_update_card.xml
  11. 1 1
      app/src/main/res/layout/dialog_update_point.xml
  12. 1 1
      app/src/main/res/layout/fragment_job_execute.xml
  13. 1 3
      app/src/main/res/values-en/strings.xml
  14. 2 4
      app/src/main/res/values-zh/strings.xml
  15. 2 4
      app/src/main/res/values/strings.xml
  16. 5 0
      data/src/main/java/com/grkj/data/data/MMKVConstants.kt
  17. 8 0
      shared/build.gradle.kts
  18. 4 0
      shared/src/main/java/com/grkj/shared/utils/i18n/CsvImporter.kt
  19. 35 0
      shared/src/main/java/com/grkj/shared/utils/i18n/I18nFormatter.kt
  20. 148 0
      shared/src/main/java/com/grkj/shared/utils/i18n/I18nManager.kt
  21. 41 0
      shared/src/main/java/com/grkj/shared/utils/i18n/I18nRepository.kt
  22. 12 0
      shared/src/main/java/com/grkj/shared/utils/i18n/I18nSource.kt
  23. 218 0
      shared/src/main/java/com/grkj/shared/utils/i18n/LanguageCatalog.kt
  24. 98 0
      shared/src/main/java/com/grkj/shared/utils/i18n/LanguageStore.kt
  25. 21 0
      shared/src/main/java/com/grkj/shared/utils/i18n/PluralRules.kt
  26. 57 0
      shared/src/main/java/com/grkj/shared/utils/i18n/databinding/I18nBindingAdapters.kt
  27. 83 0
      shared/src/main/java/com/grkj/shared/utils/i18n/importer/CsvImporter.kt
  28. 84 0
      shared/src/main/java/com/grkj/shared/utils/i18n/importer/CsvMerger.kt
  29. 45 0
      shared/src/main/java/com/grkj/shared/utils/i18n/source/AssetsCsvSource.kt
  30. 38 0
      shared/src/main/java/com/grkj/shared/utils/i18n/source/FileCsvSource.kt
  31. 116 0
      shared/src/main/java/com/grkj/shared/utils/i18n/util/CsvUtils.kt
  32. 1 0
      ui-base/src/main/java/com/grkj/ui_base/business/BleBusinessManager.kt
  33. 19 2
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleConnectionManager.kt
  34. 28 0
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleQueueDispatcher.kt
  35. 0 1
      ui-base/src/main/res/values-en/strings.xml
  36. 0 6
      ui-base/src/main/res/values-zh/strings.xml
  37. 4 3
      ui-base/src/main/res/values/ids.xml
  38. 0 1
      ui-base/src/main/res/values/strings.xml

+ 637 - 0
app/src/main/assets/i18n/zh-CN.csv

@@ -0,0 +1,637 @@
+key,type,comment,value
+i18n.language_name,text,语言自称,中文
+abnormal,text,添加修改数据界面的状态的显示文本,异常
+account_login,text,登录界面的账号密码登录的文本,用户名登录
+action_confirm,text,提示弹窗的标题,操作确认
+action_confirm_content,text,点击步骤确认时的消息,确定要执行{0}吗?
+action_failed,text,提示弹窗的标题,操作失败
+action_hint,text,提示弹窗的标题,操作提醒
+action_succeed,text,提示弹窗的标题,操作成功
+add_card_failed,text,提示弹窗的内容,添加卡片失败
+add_card_succeed,text,提示弹窗的内容,添加卡片成功
+add_colocker,text,流程模式设置,步骤功能显示,添加共锁人({0})
+add_group,text,选择点位的时候的分组添加的按钮,添加分组
+add_key_failed,text,添加钥匙失败的弹窗的内容,新增钥匙失败
+add_key_succeed,text,添加钥匙成功的弹窗的内容,新增钥匙成功
+add_lock_failed,text,添加挂锁失败的弹窗的内容,添加挂锁失败
+add_lock_succeed,text,添加挂锁成功的弹窗的内容,添加挂锁成功
+add_point_failed,text,添加隔离点失败的弹窗的内容,新增隔离点失败
+add_point_succeed,text,添加隔离点成功的弹窗的内容,新增隔离点成功
+add_rfid_token_failed,text,添加RFID失败的弹窗的内容,添加RFID标签失败
+add_rfid_token_succeed,text,添加RFID成功的弹窗的内容,添加RFID标签成功
+add_role_failed,text,添加角色失败的弹窗的内容,新增角色失败
+add_role_succeed,text,添加角色成功的弹窗的内容,新增角色成功
+add_user_succeed,text,添加用户成功的弹窗的内容,新增用户成功
+add_workstation_failed,text,添加区域失败的弹窗的内容,新增区域失败
+add_workstation_succeed,text,添加区域成功的弹窗的内容,新增区域成功
+admin_role_can_not_edit,text,管理员信息修改的提示内容,管理员角色无法编辑
+admin_username,text,初始化超级管理员时的账号的标题,管理员账号:(数字、字母、6-20位)
+all,text,下拉框的全部文本,全部
+all_hardware_tv,text,首页的全部硬件文本,全部\n硬件
+all_job_tv,text,首页的全部作业文本,全部\n作业
+all_points_tv,text,首页的全部点位文本,全部\n点位
+all_quick_entrance,text,快捷入口的配置弹窗的备选快捷入口的文本,所有快捷入口
+all_select_not_all_select,text,角色添加/修改弹窗中权限菜单选矿的文本,全选/全不选
+already_colock,text,作业执行界面的共锁人员界面的标题文本,已共锁({0})
+already_uncolock,text,作业执行界面的共锁人员界面的标题文本,已解除共锁({0})
+back,text,通用返回文本,返回
+base_info_title,text,作业创建/修改,SOP创建/修改的基本信息标题文本,基本信息
+ble_connect_fail,text,蓝牙连接失败的加载弹窗提示文本,连接失败,请重试!
+ble_connecting,text,蓝牙连接中的加载弹窗的提示文本,连接中,请稍后...
+can_not_remove_current_colocker,text,选择共锁人时只有一个共锁人时移除触发的提示文本,无法移除当前共锁人
+can_not_remove_current_locker,text,选择上锁人时,非选择人员步骤不允许移除上锁人提示文本,无法移除当前上锁人
+cancel,text,通用取消文本,取消
+cancel_countdown,text,提示框的取消倒计时文本,取消({0}秒)
+cancel_exception,text,异常管理详情/异常作业详情界面的按钮文本,取消异常
+cancel_exception_failed,text,取消异常完成之后提示弹窗文本,取消异常失败
+cancel_exception_success,text,取消异常完成之后提示弹窗文本,异常取消成功
+cancel_job,text,作业执行界面的按钮文本,取消作业
+cancel_job_tip,text,取消作业的二次确认弹窗文本,是否确认取消当前作业
+capture_tip_content,text,人脸录入时的提示文本,"1. 系统将自动拍摄照片,在拍摄过程中请确保:
+     \n · 脸部正对摄像头
+     \n · 保持适当距离,让整个脸部出现在左侧框中
+     \n · 光线充足
+     \n · 表情自然
+\n2. 拍摄完成后,您可以点击确认按钮进行提交,也可以点击重拍按钮重新进行拍摄。
+\n3. 取消录入,请点击取消按钮"
+capture_tip_title,text,录入人脸时的标题文本,录入提示
+card_already_registration,text,卡片录入时已存在卡片保存是的错误文本,卡片已录入
+card_code,text,卡片管理表头文本,卡片名称
+card_login,text,登录界面的刷卡登录文本,刷卡登录
+card_manage_card_detail_title,text,卡片管理表头文本,卡片详情
+card_manage_delete_failed,text,卡片管理删除卡片失败时的提示弹窗文本,卡片删除失败
+card_manage_delete_succeed,text,卡片管理删除卡片成功时的提示弹窗文本,卡片删除成功
+card_manage_new_card_title,text,添加卡片弹窗的标题文本,新增卡片
+card_manage_title,text,卡片管理界面的标题文本,卡片管理
+card_nfc,text,卡片管理表头文本,卡片 NFC
+change_to_standard,text,设置远程服务地址时的切换版本按钮文本,切换标准版
+check_before_unlocking,text,八大步骤(玛氏)的步骤流程名称,检查使用,取锁前检查
+check_delete_card,text,删除卡片时的二次确认弹窗文本,确定要删除选中的卡片吗?
+check_delete_job,text,删除作业时的二次确认弹窗文本,您确认要删除作业吗
+check_delete_key,text,删除钥匙时的二次确认弹窗文本,您确定要删除选中的钥匙吗
+check_delete_lock,text,删除挂锁时的二次确认弹窗文本,确定要删除选中的挂锁吗?
+check_delete_point,text,删除隔离点时的二次确认弹窗文本,您确定要删除隔离点吗
+check_delete_rfid_token,text,删除RFID标签时的二次确认弹窗文本,确定要删除选中的RFID标签吗?
+check_delete_role,text,删除角色时的二次确认弹窗文本,您确认要删除角色吗
+check_delete_sop,text,删除SOP时的二次确认弹窗文本,您确定要删除选中的SOP吗
+check_delete_user,text,删除用户时的二次确认弹窗文本,您确认要删除用户吗?
+check_delete_workflow_mode,text,删除流程模式时的二次确认弹窗文本,确定要删除选中的流程模式吗
+check_delete_workstation,text,删除区域时的二次确认弹窗文本,"您确定要删除区域\""{0}\""吗"
+check_device_info,text,初始化界面检测硬件完成之后的检测信息结果文本,"检测到未注册钥匙{0}把,未注册挂锁{1}把"
+check_key_and_lock,text,初始化界面检测硬件时的文本,正在检查钥匙和挂锁
+check_lock_is_new_device,text,仓位管理检查挂锁的加载弹窗的文本,检查挂锁是否为新硬件
+check_new_key_need_register,text,仓位管理检测到新钥匙是否注册的提示弹窗的文本,检测到新钥匙,是否注册
+check_new_lock_need_register,text,仓位管理检测到新挂锁是否注册的提示弹窗的文本,检测到新挂锁,是否注册
+close,text,通用关闭文本,关闭
+colock,text,流程模式设置,步骤功能显示,共锁
+colock_complete,text,作业执行界面添加共锁完成弹窗的文本,添加共锁完成
+colock_failed,text,作业执行界面添加共锁失败弹窗的文本,添加共锁失败
+colocker,text,通用共锁人文本,共锁人
+confirm,text,通用确认文本,确定
+confirm_cancel_exception,text,取消异常时的二次确认弹窗文本,是否确认取消异常
+confirm_create_lock_job,text,解锁-上锁任务,完成解锁步骤之后,处理异常之后的提示弹窗文本,确认是否创建上锁作业
+confirm_create_unlock_job,text,上锁-解锁任务,完成上锁步骤之后,处理异常之后的提示弹窗文本,是否创建解锁作业
+confirm_handle_exception,text,处理异常时的二次确认弹窗文本,是否确认处理异常
+confirm_to_colock,text,共锁人添加共锁时的弹窗确认文本,{0}是否确认共锁?
+confirm_to_uncolock,text,共锁人解除共锁时的弹窗确认文本,{0}是否确认解除共锁?
+continue_the_ticket,text,作业票未完成归还钥匙之后,如果不强制上传数据的提示文本,请继续完成作业票
+create_job_failed,text,处理异常之后新建作业创建时创建失败的弹窗文本,创建作业失败
+create_job_name,text,创建作业/编辑作业的基本信息中的作业名称文本,作业名称
+create_job_title,text,新建作业的标题文本,新建作业
+create_sop_job_sop,text,新建SOP作业选择Sop文本,SOP
+create_sop_job_tip,text,保存并执行作业时的二次确认弹窗,"确定要执行作业\""{0}\""吗?"
+create_sop_job_title,text,新建SOP作业标题,新建SOP作业
+create_sop_name,text,新建SOP的sop名称文本,SOP名称
+create_sop_title,text,新建SOP的标题,新建SOP
+current_job_has_cross_job,text,处理异常时的交叉作业警告弹窗,警告!当前作业存在交叉作业,是否继续处理异常
+current_role_no_user,text,流程模式编辑确认方式角色确认时,如果选择的角色无用户时提示,当前角色暂无用户
+current_slot_has_no_key,text,仓位管理检查钥匙时如果仓内没有钥匙时弹窗提示,当前仓位不存在钥匙
+current_slot_has_no_lock,text,仓位管理检查挂锁时如果仓内没有挂锁时弹窗提示,当前仓位不存在挂锁
+current_sop_has_job_in_progress,text,删除sop作业时如果存在关联作业弹窗提示文本,当前SOP存在进行中的作业
+current_ticket_report_lock_take_exception_tip,text,挂锁取出时,如果作业异常提示文本,当前作业挂锁上报异常,请归还挂锁
+current_user_has_not_face_data,text,选择人员界面,如果头像为空时点击提示,当前用户不存在人脸数据
+current_workflow_mode_error,text,进行流程模式设置界面,如果流程模式数据获取异常时提示,当前流程模式错误
+currently_no_hardware_can_be_report,text,异常上报,类型为硬件异常但是没有硬件数据时提示,当前没有硬件可以上报
+currently_no_job_can_be_report,text,异常上报,类型为作业异常但是没有作业数据时提示,当前没有作业可以上报
+currently_unable_to_lock_together,text,作业执行界面,如果步骤未在共锁步骤,刷卡提示,当前阶段无法共锁
+data_content_error,text,导入流程模式时如果解密之后的内容转换异常时提示,数据内容错误
+data_decrypt_failed,text,导入流程模式时如果数据解密失败时提示,数据解密失败
+data_file_is_corrupted,text,导入流程模式时如果数据文件的校验码错误时提示,数据文件已损坏
+data_file_not_exists,text,导入流程模式时如果数据文件不存在时提示,数据文件不存在
+date,text,时间范围选择弹窗格式,{0}年{1}月{2}日
+delete,text,通用删除文本,删除
+delete_group,text,选择点位的时候的分组删除的按钮,删除分组
+delete_success,text,流程模式和指纹删除成功的弹窗文本,删除成功
+detail,text,通用表头文本,详情
+detect_face_tip,text,人脸录入时检测到人脸的提示文本,检测到人脸,即将拍摄
+detect_port,text,仓位管理进入的加载文本,正在扫描设备......
+detect_slot,text,仓位管理的检测按钮文本,检测仓位
+device_in_detect,text,初始化硬件的时候的识别中提示文本,设备识别中
+do_you_want_to_remove_exception,text,仓位管理长按移除异常的二次确认弹窗文本,是否确认移除该异常
+doing_checking,text,步骤确认验证人脸时的加载提示文本,正在验证......
+doing_login,text,登录时的加载提示文本,正在登录······
+done_header,text,我的待办的tab文本,已处理
+edit,text,通用编辑文本,编辑
+edit_job_title,text,编辑作业的标题文本,作业详情
+edit_sop_job_title,text,编辑SOP作业的标题文本,SOP作业详情
+edit_sop_title,text,编辑SOP的标题文本,SOP详情
+end,text,选择时间范围的结束时间的文本,结束
+end_job,text,作业执行界面的按钮文本,结束作业
+end_time,text,首页的时间范围的文本,结束时间
+ensure_power_isolation,text,八大步骤(玛氏)的步骤流程名称,检查使用,能量隔离证实
+error_date_range_invalid,text,时间范围选择时的检查提示文本,开始时间不能晚于结束时间
+exception_data_not_exists,text,处理异常时,异常数据不存在的提示弹窗文本,异常数据不存在
+exception_description,text,异常管理详情的表头,异常描述:
+exception_description_tv,text,异常上报的内容标题,异常描述
+exception_detail_title,text,异常管理详情的标题,异常详情
+exception_info,text,异常管理详情的的表头,异常信息
+exception_job,text,异常作业的详情的表头,异常作业:
+exception_job_title,text,异常作业的标题,异常作业
+exception_lost,text,异常作业的数据不存在的情况下提示,异常丢失
+exception_manage_title,text,异常管理的标题,异常管理
+exception_occurrence_time,text,异常管理详情的表头,异常发生时间:
+exception_occurrence_time_header,text,异常管理的表头,发生时间
+exception_reason,text,仓位管理异常上报的弹窗信息,异常信息
+exception_release_time,text,异常管理详情的表头,异常解除时间:
+exception_report,text,仓位异常上报和异常上报的标题,异常上报
+exception_report_success,text,异常上报成功之后的弹窗提示,异常上报成功
+exception_reporter,text,异常管理详情的表头,上报人:
+exception_source,text,异常管理详情的表头,异常源:
+exception_source_tv,text,异常上报的内容标题,异常源
+exception_status,text,异常管理详情的表头,状态:
+exception_status_header,text,异常管理的表头,异常状态
+exception_type,text,异常管理详情的表头,异常类型:
+exception_type_header,text,异常管理列表的表头,异常类型
+exception_type_tv,text,异常上报的内容标题,异常类型
+expand_collapse,text,添加/修改角色的权限树的操作文本,展开/折叠
+face_can_not_process,text,虹软初始化失败时的提示文本,人脸引擎激活失败,识别暂不可用
+face_detected_do_login,text,人脸登录的加载窗提示文本,检测到人脸,正在登录······
+face_login,text,登录界面的人脸登录文本,人脸登录
+face_login_failed,text,人脸登录失败时的提示文本,人脸匹配失败,请重试
+face_login_success,text,人脸登录成功时的提示文本,人脸验证通过
+face_not_set_tip,text,用户信息的人脸设置界面未设置人脸的显示文本,您尚未设置人脸数据
+face_set_tip,text,用户信息的人脸设置界面已设置人脸的显示文本,您已设置了人脸数据
+file_not_exists,text,流程模式导入时如果指定文件不存在时提示,文件不存在
+filter,text,通用筛选文本,筛选
+fingerprint_add_success_tip,text,指纹添加成功时的弹窗文本,已成功添加指纹数据
+fingerprint_code,text,指纹列表的表头,指纹编号
+fingerprint_code_str,text,指纹列表的数据前缀,指纹_{0}
+fingerprint_delete_confirm_tip,text,指纹列表指纹项删除指纹的二次确认弹窗文本,确定要删除{0}吗?
+fingerprint_delete_selected_confirm_tip,text,指纹列表删除指纹的二次确认弹窗文本,确定要删除选中的指纹吗?
+fingerprint_login,text,登录界面的指纹登录文本,指纹登录
+fingerprint_login_failed,text,登录界面的指纹登录失败提示文本 ,指纹识别失败,请重试
+fingerprint_login_success,text,登录界面的指纹登录成功提示文本,指纹验证通过
+fingerprint_scan_tip,text,录入指纹时的提示录入提示文本,请连续按压{0}次指纹识别区
+finish_job_tip,text,作业执行界面结束作业的二次确认弹窗文本,是否确认结束当前作业
+finish_the_job,text,作业执行界面的结束作业按钮文本,结束作业
+get_key_info_fail,text,钥匙数据获取失败时的提示文本,获取钥匙信息失败
+go_locking,text,作业执行界面的去上锁按钮文本,去上锁
+go_unlocking,text,作业执行界面的去解锁按钮文本,去解锁
+group_at_least_has_one_point,text,选择点位确认按钮检查分组点位的错误提示文本,每个分组至少需要存在一个点位
+group_job_in_progress,text,分组已经获取钥匙未归还的情况再次点击的提示文本,分组作业进行中
+group_name_must_not_empty,text,选择点位分组名称为空时保存的错误提示文本,分组名称不能为空
+handle,text,我的待办的处理按钮文本,处理
+handle_colock,text,我的待办处理共锁时的二次确认弹窗文本,请确认是否要进行添加共锁
+handle_exception,text,异常详情和异常作业的处理异常的按钮文本,处理异常
+handle_exception_success,text,异常处理成功的弹窗文本,异常处理成功
+handle_exception_will_release_all_colock,text,异常作业的已经共锁的作业结束时的警告弹窗文本,警告!处理异常将移除所有共锁,请确认是否继续
+handle_failed,text,异常处理失败的弹窗提示文本,处理失败
+handle_lock_take_key,text,我的待办上锁取钥匙时的二次确认弹窗文本,确认获取钥匙进行上锁吗?
+handle_release_colock,text,我的待办的解除共锁时的二次确认弹窗文本,请确认是否要进行解除共锁
+handle_step_confirm,text,我的待办的步骤确认时的二次确认弹窗文本,请确认是否完成[{0}]
+handle_time,text,异常管理详情的表头,处理时间:
+handle_time_custom_time_range,text,我的待办的已处理的时间筛选的选择项文本,自定义区间
+handle_time_last_30_days,text,我的待办的已处理的时间筛选的选择项文本,近30天
+handle_time_last_7_days,text,我的待办的已处理的时间筛选的选择项文本,近7天
+handle_unknown,text,我的待办的处理项类型异常的提示文本,当前处理类型未知,无法处理
+handle_unlock_take_key,text,我的待办解锁取钥匙时的二次确认弹窗文本,确认获取钥匙进行解锁吗?
+hardware_in_use_tv,text,首页使用中的硬件文本,使用中\n的硬件
+hardware_info,text,仓位异常上报的信息展示,硬件信息: {0}
+hardware_key,text,仓位异常上报的硬件类型文本,钥匙
+hardware_lock,text,仓位异常上报的硬件类型文本,挂锁
+hardware_unknown,text,仓位异常上报的硬件类型文本,未知
+has_job_in_progress,text,作业管理删除作业时有作业存在时的弹窗文本,存在正在进行中的作业
+has_locked,text,作业执行界面的点位上锁状态文本,已上锁
+has_user_in_progress_job,text,删除用户时如果关联用户的作业有进行中的时候弹窗文本,有用户在进行的作业中
+home_overview_data_title,text,首页总览数据标题,总览数据
+home_realtime_data_title,text,首页试试数据标题,实时数据
+import_str,text,通用导入文本,导入
+import_success,text,通用导入成功文本,导入成功
+in_progress_job_manage_title,text,进行中的作业的标题,进行中的作业
+init_card_registration_step_hint,text,初始化卡片注册时的提示文本,请在读卡器上刷卡
+init_card_registration_step_tip,text,初始化卡片注册时的标题文本,识别并录入卡片
+init_device_registration_key_and_lock_complete_step_hint,text,初始化硬件的时候的扫描完成的提示文本,扫描完成
+init_device_registration_key_and_lock_step_hint,text,初始化硬件的时候的扫描中的提示文本,请等待系统识别钥匙和挂锁
+init_device_registration_key_and_lock_step_tip,text,初始化硬件的时候的标题文本,识别钥匙和挂锁
+init_point_rfid_registration_step_hint,text,初始化点位注册的时候的提示文本,请在读卡器上刷点位RFID标签
+init_point_rfid_registration_step_tip,text,初始化点位注册的时候的标题文本,识别并录入点位RFID标签
+init_set_admin_account_step,text,初始化设置管理员账号时的步骤序号,1
+init_set_admin_account_step_hint,text,初始化设置管理员账号时的提示文本,请设置管理员账号密码
+init_set_admin_account_step_tip,text,初始化设置管理员账号时的标题文本,设置管理员账号
+init_set_remote_server_step_hint,text,设置远程服务器地址的提示文本,请设置服务器的地址和端口
+init_set_remote_server_step_tip,text,设置远程服务器地市的标题文本,配置服务器
+insert,text,通用新增文本,新增
+invalid_card,text,作业执行界面和我的待办界面的共锁人刷卡时卡片无效的提示文本,卡片无效
+invalid_user,text,作业执行界面和我的待办界面的共锁人刷卡时用户不存在的提示文本,用户不存在
+item_my_todo_complete_time_title,text,我的待办信息的头文本,完成时间:
+item_my_todo_current_operation_title,text,我的待办信息的头文本,当前操作:
+item_my_todo_current_step_title,text,我的待办信息的头文本,当前步骤:
+item_my_todo_job_name_title,text,我的待办信息的头文本,相关作业:
+job_already_finished,text,作业执行界面,收到作业被结束的消息的提示文本,该作业已被结束
+job_canceled,text,作业管理已取消的作业点击提示文本,作业已取消
+job_card_login_failed,text,工卡登录失败的提示文本,工卡无效
+job_card_login_success,text,工卡登录成功的提示文本,工卡识别成功
+job_card_not_set_tip,text,设置工卡界面,还没有设置工卡时的文本信息,您尚未设置工卡
+job_card_scan_tip,text,设置工卡界面,设置工卡的提示文本,请在读卡器上读卡
+job_card_set_tip,text,设置工卡界面,已经设置工卡时的文本信息,您已设置了工卡数据
+job_create_and_execute_failed,text,保存并执行作业失败时的弹窗提示文本,作业执行失败
+job_create_and_execute_succeed,text,保存并执行作业成功时的弹窗提示文本,作业开始执行
+job_create_failed,text,保存作业失败时的弹窗提示文本,作业保存失败
+job_create_succeed,text,保存作业成功时的弹窗提示文本,作业保存成功
+job_execute_colocker_colock_status_title,text,作业执行界面和异常作业界面的共锁人标题,共锁人员共锁状态
+job_execute_lock_status_title,text,作业执行界面和异常作业界面的点位标题,隔离点锁定状态
+job_execute_step_description,text,作业执行界面和异常作业界面的操作说明标题,操作说明({0})
+job_execute_tab_title_colock,text,作业执行界面和异常作业界面的共锁人TAB标题,共锁
+job_execute_tab_title_lock,text,作业执行界面和异常作业界面的点位TAB标题,锁定
+job_execute_title,text,作业执行界面标题,作业执行
+job_finished,text,作业管理的已结束作业点击提示文本,作业已结束
+job_lost,text,作业执行界面的作业数据不存在的文本,作业丢失
+job_manage_delete_failed,text,作业管理删除作业失败时的弹窗提示文本,无法删除选中的作业
+job_manage_delete_succeed,text,作业管理删除作业成功时的弹窗提示文本,删除选中的作业成功
+job_manage_title,text,作业管理标题,作业管理
+job_name,text,作业观猎列表表头,作业名称
+job_save_and_execute_tip,text,保存并执行作业时二次确认弹窗文本,"确定要执行作业\""{0}\""吗?"
+job_save_tip,text,保存作业二次确认弹窗文本,"确定要保存作业\""{0}\""吗?"
+job_status,text,锁定中的点位表头,作业状态
+job_workstation,text,创建/修改作业的基本信息的文本,作业区域
+key_exception_tag,text,还钥匙的时候如果钥匙异常提示,该钥匙已被标记异常
+key_in_use,text,钥匙管理列表删除钥匙时如果钥匙使用中提示,钥匙正在使用
+key_info_already_exists,text,仓位管理检查钥匙仓位注册时如果钥匙信息已存在提示,钥匙信息已存在
+key_is_in_failure_mode,text,钥匙状态查询为故障的时候输出到日志,钥匙处于故障模式
+key_mac,text,添加钥匙弹窗的信息文本,钥匙MAC
+key_manage_delete_failed,text,钥匙管理删除失败时弹窗文本,钥匙删除失败
+key_manage_delete_succeed,text,钥匙管理删除成功时弹窗文本,钥匙删除成功
+key_manage_key_detail_title,text,钥匙详情标题,钥匙详情
+key_manage_new_key_title,text,新增钥匙标题,新增钥匙
+key_manage_title,text,钥匙管理标题,钥匙管理
+key_name,text,钥匙管理表头,钥匙名称
+key_nfc,text,钥匙管理表头,钥匙NFC
+key_not_exists,text,切换工作模式之后,没有找到钥匙信息时提示,钥匙不存在
+key_return_success,text,钥匙归还之后信息上报成功之后提示,钥匙归还成功
+key_return_tip,text,上报点位信息异常时提示文本,作业票尚未完成,禁止归还钥匙
+key_take_error_tip,text,获取挂锁之后,获取钥匙信息失败时提示,钥匙分配失败,请检查硬件状态
+loading_data,text,通用数据加载弹窗文本,数据加载中
+loading_device,text,仓位管理加载中文本,正在加载硬件......
+loading_msg_get_ticket_status_start,text,开始读取作业票加载中文本,正在读取钥匙作业票
+loading_msg_return_key_start,text,开始连接钥匙加载中文本,开始连接钥匙,请稍候······
+lock,text,流程模式设置,步骤功能显示,上锁
+lock_already_exists,text,仓位管理检查挂锁和挂锁列表添加挂锁信息存在时提示,挂锁信息已存在
+lock_code,text,挂锁列表表头,挂锁编号
+lock_exception_tag,text,还挂锁时挂锁异常提示,该挂锁已被标记异常
+lock_in_use,text,挂锁管理删除挂锁时,如果挂锁使用中提示,挂锁正在使用中
+lock_is_not_enough,text,去上锁是,挂锁数量不足时提示,锁具数量不足
+lock_key_return_tip,text,还钥匙时,如果作业票未完成提示是否强制上传,作业票尚未完成,是否强制上传数据
+lock_manage_delete_failed,text,挂锁删除失败时提示,挂锁删除失败
+lock_manage_delete_succeed,text,挂锁删除成功时提示,挂锁删除成功
+lock_manage_lock_detail_title,text,挂锁详情标题,挂锁详情
+lock_manage_new_lock_title,text,新增挂锁标题,新增挂锁
+lock_manage_title,text,挂锁管理标题,挂锁管理
+lock_name,text,挂锁管理表头,挂锁名称
+lock_nfc,text,挂锁管理表头,挂锁 NFC
+lock_status,text,作业执行界面挂锁信息表头,上锁状态
+lock_take_report_fail,text,挂锁取出上报异常时提示信息,挂锁取出上报失败
+locked_points_title,text,锁定中的点位标题,锁定中的点位
+locked_points_tv,text,首页锁定中的点位文本,锁定中\n的点位
+locker,text,通用上锁人文本,上锁人
+login,text,通用登录文本,登录
+login_tip,text,登录界面提示文本,请输入用户名和密码或者刷卡进行登录
+loto,text,系统主标题,智能锁控系统
+loto_en,text,系统副标题,Intelligent Lock Control System
+manage_filter_status,text,添加/修改弹窗状态文本,状态
+manage_role_function_permission,text,角色管理功能权限文本,功能权限
+member_info_title,text,人员信息标题,人员信息
+move_down,text,区域管理下移按钮文本,下移
+move_up,text,区域管理上移按钮文本,上移
+my_todo_title,text,我的待办标题,我的待办
+navigate_to_step,text,流程模式设置,步骤功能显示,跳转到第{0}步
+new_device,text,新设备标题文本,New
+new_group,text,选择点位,默认新增分组名称前缀,新分组{0}
+new_password,text,重置密码界面新密码文本,新密码(数字、字母、特殊符号、6-20位)
+new_password_and_repeat_new_password_not_same,text,重置密码界面重复密码校验提示文本,新密码与重复新密码不一致
+new_password_cannot_be_the_same_as_the_old_password,text,重置密码界面新旧密码校验提示文本,新密码与旧密码不能相同
+next,text,初始化界面下一步按钮文本,下一步
+nickname,text,通用姓名文本,姓名
+no_available_key,text,作业执行界面去上锁没有钥匙时弹窗提示文本,暂无可用钥匙
+no_data,text,列表界面没有数据时默认展示信息文本,暂无数据
+no_goto_step,text,流程模式设置,步骤功能显示,无跳转
+no_permission_to_handle,text,作业执行界面,进行操作时权限不通过提示文本,您暂无权限操作当前作业票
+no_response_board_exists,text,硬件通信时响应超时提示文本,存在未响应的主板
+normal,text,通用正常文本,正常
+not_group_can_lock,text,所有分组上锁完成之后点击去上锁按钮提示,当前无分组可上锁
+not_group_can_unlock,text,所有分组解锁完成之后点击去解锁按钮提示,当前无分组可解锁
+not_in_slot,text,仓位管理长按仓位检查硬件不存在时提示,未在仓位
+not_save_tip,text,创建/修改作业,SOP,SOP作业返回时通用确认弹窗文本,数据还没有保存,您确定要放弃保存,离开当前页面吗?
+number,text,仓位异常上报信息文本,编号: 
+old_password,text,重置密码旧密码文本,旧密码
+old_password_error,text,重置密码旧密码校验失败提示以文本,旧密码错误
+one_key_cancel,text,异常管理列表按钮文本,一键取消
+one_key_handle,text,异常管理列表按钮文本,一键处理
+ongoing_job_tv,text,首页进行中的作业文本,进行中\n的作业
+only_one_person_allowed,text,人脸录入时提示文本,请保持单人入镜
+operation,text,列表表头,操作
+password_and_repeat_password_not_same,text,重置密码和初始化设置管理员账号时密码与重复密码不一致时提示文本,密码与重复密码不一致
+password_regex_tip,text,重置密码和初始化设置管理员账号时密码不符合规范提示文本,密码不符合要求
+phone,text,通用电话文本,电话
+please_do_colock,text,作业执行界面提示文本,请共锁人完成共锁
+please_do_uncolock,text,作业执行界面提示文本,请共锁人解除共锁
+please_done_operation,text,作业执行界面点击未完成的非当前步骤时提示,请先完成{0}
+please_go_locking,text,作业执行界面提示文本,请上锁员执行去上锁操作
+please_go_unlocking,text,作业执行界面提示文本,请上锁员执行去解锁操作
+please_input_account,text,输入框提示文本和数据校验错误提示文本,请输入用户名
+please_input_admin_username,text,输入框提示文本和数据校验错误提示文本,请输入管理员账号
+please_input_area,text,输入框提示文本和数据校验错误提示文本,请输入区域
+please_input_card_code,text,输入框提示文本和数据校验错误提示文本,请输入工卡
+please_input_card_nfc,text,输入框提示文本和数据校验错误提示文本,请输入卡片 NFC
+please_input_correct_phone,text,输入框提示文本和数据校验错误提示文本,请输入正确的手机号
+please_input_exception_reason,text,输入框提示文本和数据校验错误提示文本,请输入异常原因
+please_input_job_name,text,输入框提示文本和数据校验错误提示文本,请输入作业名称
+please_input_key_mac,text,输入框提示文本和数据校验错误提示文本,请输入钥匙MAC
+please_input_key_name,text,输入框提示文本和数据校验错误提示文本,请输入钥匙名称
+please_input_key_nfc,text,输入框提示文本和数据校验错误提示文本,请输入钥匙NFC
+please_input_key_word,text,输入框提示文本和数据校验错误提示文本,请输入关键字
+please_input_lock_code,text,输入框提示文本和数据校验错误提示文本,请输入挂锁编号
+please_input_lock_nfc,text,输入框提示文本和数据校验错误提示文本,请输入挂锁 NFC
+please_input_new_password,text,输入框提示文本和数据校验错误提示文本,请输入新密码
+please_input_nickname,text,输入框提示文本和数据校验错误提示文本,请输入姓名
+please_input_old_password,text,输入框提示文本和数据校验错误提示文本,请输入旧密码
+please_input_password,text,输入框提示文本和数据校验错误提示文本,请输入密码
+please_input_permission_characters,text,输入框提示文本和数据校验错误提示文本,请输入权限字符
+please_input_phone,text,输入框提示文本和数据校验错误提示文本,请输入电话
+please_input_point_function,text,输入框提示文本和数据校验错误提示文本,请输入隔离点作用
+please_input_point_name,text,输入框提示文本和数据校验错误提示文本,请输入隔离点名称
+please_input_remark,text,输入框提示文本和数据校验错误提示文本,请输入备注
+please_input_remote_server_address,text,输入框提示文本和数据校验错误提示文本,请输入服务地址
+please_input_repeat_new_password,text,输入框提示文本和数据校验错误提示文本,请重复新密码
+please_input_repeat_password,text,输入框提示文本和数据校验错误提示文本,请输入重复密码
+please_input_rfid,text,输入框提示文本和数据校验错误提示文本,请输入 RFID 标签
+please_input_rfid_code,text,输入框提示文本和数据校验错误提示文本,请输入 RFID 编号
+please_input_rfid_tag,text,输入框提示文本和数据校验错误提示文本,请输入RFID标签
+please_input_role_name,text,输入框提示文本和数据校验错误提示文本,请输入角色名称
+please_input_sop_name,text,输入框提示文本和数据校验错误提示文本,请输入SOP名称
+please_input_step_description,text,输入框提示文本和数据校验错误提示文本,请输入步骤操作说明
+please_input_step_title,text,输入框提示文本和数据校验错误提示文本,请填写步骤标题
+please_input_step_title_short,text,输入框提示文本和数据校验错误提示文本,请输入步骤标题缩写
+please_input_username,text,输入框提示文本和数据校验错误提示文本,请输入登录名
+please_input_workstation_name,text,输入框提示文本和数据校验错误提示文本,请输入区域名称
+please_must_select_at_least_one_point,text,创建作业,SOP,SOP作业保存时如果分组点位不足时提示,您至少需要添加一个点位
+please_press_fingerprint_again,text,设置指纹时提示,请再次按压指纹
+please_re_press_fingerprint_again,text,设置指纹时如果3次全错误时提示,请重新按压指纹
+please_return_key_after_locking,text,作业执行界面提示文本,请上锁员完成上锁后,归还钥匙
+please_return_key_after_unlocking,text,作业执行界面提示文本,请上锁员完成解锁后,归还钥匙
+please_scan_face,text,登录弹窗提示文本,请刷脸
+please_scan_fingerprint,text,登录弹窗提示文本,请刷指纹
+please_select_area,text,删除区域时未选择区域的提示文本,请选择区域
+please_select_card,text,删除卡片时未选择卡片的提示文本,请选择卡片
+please_select_card_username,text,保存卡片时未选择用户的提示文本,请选择用户名称
+please_select_colocker,text,选择人员界面未选择共锁人时保存提示文本,请选择共锁人
+please_select_exception_description,text,异常上报检查数据时提示文本,请选择异常描述
+please_select_exception_source,text,异常上报检查数据时提示文本,请选择异常源
+please_select_exception_type,text,异常上报检查数据时提示文本,请选择异常类型
+please_select_flow_mode,text,选择提示文本和数据校验错误提示文本,请选择流程模式
+please_select_group,text,去上锁选择标题,请选择分组
+please_select_handle_time,text,我的待办已处理时间选择提示文本,请选择处理时间
+please_select_job,text,作业管理删除作业未选择提示文本,请选择作业
+please_select_job_workstation,text,选择提示文本和数据校验错误提示文本,请选择作业区域
+please_select_key,text,钥匙管理删除钥匙未选择提示文本,请选择钥匙
+please_select_lock,text,挂锁管理删除挂锁未选择提示文本,请选择挂锁
+please_select_locker,text,选择人员确认数据时未选择上锁人提示,请选择上锁人
+please_select_member,text,作业执行界面提示文本,您可以选择添加人员
+please_select_point,text,选择点位确认数据时未选择点位提示,请选择隔离点
+please_select_power_type,text,添加点位时能量源提示文本和未选择提示文本,请选择能量源
+please_select_process_application,text,异常上报时处理申请提示文本和未选择提示文本,请选择处理申请
+please_select_rfid_token,text,添加更新点位数据时rfid未选择提示和提示文本,请选择RFID标签
+please_select_role,text,添加修改用户时未选择角色文本和选择提示文本,请选择角色
+please_select_sop,text,创建修改SOP作业时的提示文本和未选择提示文本,请选择SOP
+please_select_sop_workstation,text,创建修改SOP作业时的提示文本和未选择提示文本,请选择SOP区域
+please_select_start_time,text,首页点击结束时间未选择开始时间时提示文本,请先选择开始时间
+please_select_status,text,添加用户时未选择状态时提示,请选择状态
+please_select_step_confirm_member,text,流程模式设置,确认数据时提示,请选择执行确认人员
+please_select_step_confirm_role,text,流程模式设置,确认数据时提示,请选择执行确认角色
+please_select_step_confirm_type,text,流程模式设置,确认数据时提示,请选择执行确认方式
+please_select_user,text,用户管理删除用户未选择时提示,请选择用户
+please_select_workflow_mode,text,创建作业,SOP时未选择流程模式提示,请选择流程模式
+please_select_workstation,text,创建作业,SOP时未选择区域提示,请选择区域
+please_swipe_card,text,登录弹窗提示文本,请刷卡
+please_take_out_ready_device_first,text,同一个作业去上锁还未取出设备,再次上锁时提示,请先取出已开卡扣的设备
+please_wait_ticket_name_lock_complete,text,交叉点位存在上锁时,有任务要去解锁时提示,请等待[{0}]上锁完成
+point_detail,text,我的待办点位明细按钮文本,点位明细
+point_in_use,text,点位删除时检查,点位使用中提示,点位正在使用无法修改
+point_info_title,text,点位信息标题文本,点位信息
+point_list_title,text,点位选择界面标题,点位清单
+point_manage_add_title,text,点位管理添加点位标题,添加点位
+point_manage_delete_failed,text,点位管理删除失败时提示,无法删除隔离点
+point_manage_delete_succeed,text,点位管理删除成功时提示,隔离点删除成功
+point_manage_point_function,text,点位管理隔离点作用表头,隔离点作用
+point_manage_point_group,text,作业执行界面点位表头,分组名称
+point_manage_point_name,text,添加更新筛选点位显示文本,隔离点名称
+point_manage_point_power_type,text,添加更新筛选点位显示文本,能量源
+point_manage_rfid,text,添加更新筛选点位显示文本,RFID
+point_manage_rfid_tag,text,添加更新筛选点位显示文本,RFID 标签
+point_manage_title,text,点位管理标题,点位管理
+point_manage_update_title,text,修改点位标题,修改点位
+point_manage_workstation,text,添加更新筛选点位显示文本,区域
+point_name_tv,text,添加更新筛选点位显示文本,隔离点
+power_isolation_way,text,添加更新筛选点位显示文本,确认隔离方式
+preset_workflow_can_not_delete,text,删除流程模式时如果流程模式时预设弹窗提示文本,预设流程模式无法删除
+previous,text,初始化界面上一步按钮我文本,上一步
+process_application_tv,text,异常上报显示文本,处理申请
+quick_entrance_most_set_tip,text,快捷入口配置提示文本,快捷入口最多设置8个
+quick_entrance_title,text,快捷入口配置弹窗标题,快捷入口配置
+re_recognize,text,捕捉人脸按钮文本,重新识别
+real_person_verification_required,text,捕捉人脸提示文本,请保持真人操作
+recapture,text,捕捉人脸按钮文本,重拍
+recognize_work_content,text,八大步骤(玛氏)的步骤流程名称,检查使用,识别工作内容
+recognized_card_rfid,text,初始化注册卡片表头,已识别的卡片RFID
+recognized_point_rfid,text,初始化注册RFID表头,已识别的点位RFID
+reduce_colocker,text,流程模式设置,步骤功能显示,减少共锁人({0})
+register_failed,text,仓位管理注册硬件失败时弹窗文本,注册失败
+register_success,text,仓位管理注册硬件成功时弹窗文本,注册成功
+release_colocker,text,流程模式设置,步骤标题,解除共锁
+remark,text,通用备注文本,备注
+repeat_new_password,text,重置密码界面,重新密码显示文本,重复新密码(数字、字母、特殊符号、6-20位)
+repeat_password,text,初始化管理员界面,重复密码显示文本,重复密码:(数字、字母、特殊符号、6-20位)
+reset,text,通用重置文本,重置
+reset_data_tv,text,设置人脸和设置工卡,重新设置按钮文本,点击重设
+reset_password_title,text,重置密码标题,重置密码
+reset_user_password_failed,text,重置密码失败弹窗提示文本,用户密码重置失败
+reset_user_password_succeed,text,重置密码成功弹窗提示文本,用户密码重置成功
+restart_app_after_set,text,设置远程地址完成之后自动重启提示,App将在设置完成后重启
+rfid,text,添加RFID显示文本,RFID 标签
+rfid_already_bind,text,添加点位时如果RFID已被绑定是弹窗显示,该Rfid标签已被绑定
+rfid_already_registration,text,初始化注册RFID,添加修改RFID时如果RFID已存在提示文本,RFID标签已录入
+rfid_code,text,RFID列表表头,RFID 编号
+rfid_in_use,text,修改RFID时如果RFID使用中无法进行修改提示,RFID标签使用中,无法修改
+rfid_name,text,RFID列表表头,RFID编号
+rfid_token_manage_delete_failed,text,删除RFID失败时提示,RFID标签删除失败
+rfid_token_manage_delete_succeed,text,删除RFID成功时提示,RFID标签删除成功
+rfid_token_manage_new_rfid_token_title,text,新增RFID标题,新增 RFID 标签
+rfid_token_manage_rfid_token_detail_title,text,修改RFID标题,RFID标签详情
+rfid_token_manage_title,text,RFID管理标题,RFID管理
+role_in_preset_tip,text,删除预设角色时检查是否是预设角色,是的话无法删除提示,预设角色不允许删除
+role_in_use,text,删除角色时检查角色是否使用中,是的话提示,角色已有作业在使用
+role_key_already_exists,text,添加角色时重复角色权限字符检测提示,该角色权限字符已存在
+role_manage_add_title,text,添加角色标题,添加角色
+role_manage_delete_failed,text,删除角色失败时提示,无法删除角色
+role_manage_delete_succeed,text,角色删除成功时提示,角色已删除
+role_manage_permission_string,text,权限字符显示文本,权限字符
+role_manage_role_name,text,角色名称显示文本,角色名称
+role_manage_role_num,text,角色编号显示文本,角色编号
+role_manage_title,text,角色管理标题,角色管理
+save,text,通用保存文本,保存
+save_and_execute,text,通用保存并执行文本,保存并执行
+save_sop_check,text,生成SOP选择框文本,生成SOP
+save_success,text,保存成功通用显示文本,保存成功!
+scan_complete_app_restarting,text,自动扫描串口完成之后自动重启提示,扫描完成,APP将自动重启
+select,text,通用选择文本,选择
+select_colocker_tip,text,选择人员界面选择共锁人提示文本,请在以下人员中选择共锁人
+select_coloker,text,选择人员界面未选择共锁人确认时提示文本,请选择共锁人
+select_group_tip,text,选择点位界面的分组选择操作提示文本,点击分组空白区域进行选中
+select_locker,text,选择人员界面未选择上锁人确认时提示文本,选择上锁人
+select_locker_tip,text,选择人员界面选择上锁人提示文本,请在以下人员中选择[{0}]上锁人
+select_member_title,text,选择人员标题,选择人员
+select_point_title,text,选择点位标题,选择点位
+selected_point_already_in_use,text,删除点位时,检查到存在使用中的点位是提示,存在使用中的点位
+selected_point_info_title,text,默认分组名称,已选择的点位信息
+selected_quick_entrance,text,快捷入口弹窗界面,已选择的标题提示文本,已配置的快捷入口(最多添加8个快捷入口,可拖拽排序)
+selected_rfid_in_use,text,删除RFID时检查到存在使用中的RFID时提示,存在正在使用的RFID标签
+send_ticket_fail,text,作业票下发失败时提示文本,作业票下发失败
+sending_ticket,text,作业票下发中加载弹窗文本,工作票下发中······
+server_address,text,设置服务器地址显示文本,服务地址
+server_address_error,text,设置服务器地址,服务器地址错误提示文本,服务器地址错误
+set_colocker,text,流程模式设置,功能菜单显示,设置共锁人
+set_data_tv,text,设置人脸和设置工卡,未设置时按钮文本,点击设置
+set_face_title,text,设置人脸标题,设置人脸
+set_fingerprint_title,text,设置指纹标题,设置指纹
+set_job_card_title,text,设置工卡标题,设置工卡
+set_locker,text,流程模式设置,功能菜单显示,设置上锁人
+set_password,text,初始化设置管理员,设置密码文本显示,设置密码:(数字、字母、特殊符号、6-20位)
+settings,text,通用设置文本,设置
+show_member_when_selected_sop,text,创建SOP作业人员信息展示区域提示,选择SOP后,将自动展示SOP的人员信息。
+show_points_when_selected_sop,text,创建SOP作业点位信息展示区域提示,选择SOP后,将自动展示SOP的点位信息。
+shutdown,text,八大步骤(玛氏)的步骤流程名称,检查使用,操作停机
+ski_step,text,初始化工卡,按钮文本,跳过该步骤
+skip_and_complete,text,初始化RFId,按钮文本,跳过并完成
+slot_exception_tag,text,归还钥匙时检查锁仓异常时提示,该锁仓已被标记异常
+slots_exception_report,text,锁仓异常上报标题,仓位异常上报
+slots_manage_title,text,仓位管理标题,仓位管理
+sop_create_failed,text,SOP创建失败时弹窗文本,SOP创建失败
+sop_create_succeed,text,SOP创建成功时弹窗文本,SOP创建成功
+sop_job_save_and_execute_failed,text,SOP执行失败时弹窗文本,SOP作业执行失败
+sop_job_save_and_execute_succeed,text,SOP执行成功时弹窗文本,SOP作业开始执行
+sop_job_save_failed,text,SOP保存失败时弹窗文本,SOP作业保存失败
+sop_job_save_succeed,text,SOP保存成功时弹窗文本,SOP作业保存成功
+sop_manage_delete_failed,text,删除SOP时检查是否有正在进行中的作业管理,存在时提示,无法删除选中的SOP
+sop_manage_delete_succeed,text,删除选中的SOP成功时弹窗文本,删除选中的SOP成功
+sop_manage_sop_name,text,SOP列表表头,SOP名称
+sop_manage_title,text,SOP管理标题,SOP管理
+sop_manage_workstation,text,SOP管理表头,所属岗位
+sop_save_failed,text,SOP保存失败时弹窗文本,SOP保存失败
+sop_save_succeed,text,SOP保存成功时弹窗文本,SOP保存成功
+sop_save_tip,text,SOP保存时二次确认弹窗文本,"确定要保存\""{0}\""吗?"
+sop_workstation,text,创建修改SOP作业界面SOP区域选择文本,SOP区域
+start,text,初始化欢迎界面开始按钮文本,开始
+start_detect_key_slot,text,仓位管理检测钥匙仓位开始时加载弹窗文本,开始检测钥匙仓位
+start_detect_lock_slot,text,仓位管理检测挂锁仓位开始时加载弹窗文本,开始检测挂锁仓位
+start_scan_key_mac,text,仓位管理检测钥匙仓位获取钥匙信息时加载弹窗文本,开始扫描钥匙信息
+start_time,text,首页时间范围开始时间,开始时间
+end_time_must_large_then_start_time,text,选择开始时间和结束时间时判断两个时间的大小提示文本,结束时间必须大于开始时间
+start_tip,text,初始化欢迎界面提示文本,根据提示对系统进行初始化
+start_to_send_ticket,text,开始下发工作票加载弹窗文本,开始下发工作票······
+status,text,通用状态文本,状态
+step_confirm_failed,text,作业执行界面步骤确认失败提示文本,步骤确认失败
+take_out_key,text,作业票下发完成之后加载弹窗显示文本,请取出钥匙
+take_out_key_tip,text,作业票下发完成之后加载弹窗显示文本,请从打开的钥匙仓取出钥匙
+take_out_lock_tip,text,去上锁发锁之后加载弹窗显示需要获取的钥匙文本,请从打开的锁仓取出锁,还有{0}把待取出
+take_out_rest_locks,text,去上锁发锁之后再次选择分组提示先取出已经打开卡扣的挂锁文本,请取出剩余开启卡扣的挂锁
+tec_support,text,技术支持文本,温州博士安全用品有限公司
+the_verification_file_not_exists,text,流程模式导入时解压文件的校验文件不存在时提示,校验文件不存在
+ticket_data_error,text,获取工作票数据之后数据转换异常时提示,工作票数据损坏
+ticket_get_failed,text,还钥匙之后钥匙连接失败时提示,作业票获取失败
+ticket_lost,text,获取的工作票已经取消或结束时进行判断,作业票不存在
+time_frame_tv,text,首页时间范围显示文本,时间范围
+todo_header,text,我的待办处理中TAB表头,处理中
+turn_off,text,仓位管理开关按钮文本,关
+turn_on,text,仓位管理开关按钮文本,开
+turn_read,text,仓位管理RFID读取按钮文本,读
+uncolock_complete,text,作业执行界面和我的待办界面解除共锁成功时提示,解除共锁成功
+uncolock_failed,text,作业执行界面和我的待办界面解除共锁失败时提示,解除共锁失败
+unlock,text,流程模式设置,步骤文本,解锁
+unlock_and_restore_switch,text,八大步骤(玛氏)的步骤流程名称,检查使用,拆锁恢复开关
+unzip,text,流程模式导入解压文件时加载弹窗显示,解压中……{0}
+update_card_failed,text,修改卡片信息失败时弹窗提示,更新卡片失败
+update_card_succeed,text,修改卡片信息成功时弹窗提示,更新卡片成功
+update_key_failed,text,修改钥匙信息失败时弹窗提示,更新钥匙失败
+update_key_succeed,text,修改钥匙信息成功时弹窗提示,更新钥匙成功
+update_lock_failed,text,修改挂锁信息失败时弹窗提示,更新挂锁失败
+update_lock_succeed,text,修改挂锁信息成功时弹窗提示,更新挂锁成功
+update_point_failed,text,修改点位信息失败时弹窗提示,保存点位失败
+update_point_succeed,text,修改点位信息成功时弹窗提示,保存点位成功
+update_rfid_token_failed,text,修改RFID信息失败时弹窗提示,更新RFID标签失败
+update_rfid_token_succeed,text,修改RFID信息成功时弹窗提示,更新RFID标签成功
+update_role_failed,text,修改角色信息失败时弹窗提示,角色更新失败
+update_role_succeed,text,修改角色信息成功时弹窗提示,角色更新成功
+update_user_failed,text,修改用户信息失败时弹窗提示,用户更新失败
+update_user_succeed,text,修改用户信息成功时弹窗提示,用户更新成功
+update_workstation_failed,text,修改区域信息失败时弹窗提示,更新区域失败
+update_workstation_succeed,text,修改区域信息成功时弹窗提示,更新区域成功
+user_already_exists,text,添加用户,检测用户信息是否存在,存在时提示,用户已存在
+user_info_title,text,个人信息标题,个人信息
+user_manage_area,text,添加修改用户区域文本,区域
+user_manage_card_code,text,添加修改用户工卡文本,工卡
+user_manage_delete_failed,text,删除用户时检查用户是否在进行中的作业,存在则提示无法删除,无法删除用户
+user_manage_delete_succeed,text,用户删除成功时提示,用户已删除
+user_manage_filter_activate,text,用户筛选状态文本,正常
+user_manage_filter_deactivate,text,用户筛选状态文本,停用
+user_manage_filter_title,text,筛选条件标题,筛选条件
+user_manage_new_user_title,text,新增用户标题,新增用户
+user_manage_role,text,添加修改用户角色文本,角色
+user_manage_title,text,用户管理标题,用户管理
+user_manage_user_detail_title,text,用户详情标题,用户详情
+user_manage_view,text,用户管理列表表头,查看
+user_name,text,添加修改用户用户名文本,登录名
+username,text,通用用户名称文本,用户名称
+username_or_password_error,text,账号密码登录时,账号密码错误提示,账号或密码错误
+username_passowrd_login_success,text,账号密码登录成功提示,账号密码验证通过
+username_password_not_exists,text,账号密码登录时,如果账号不存在提示,账号密码不存在
+username_regex_tip,text,初始化管理员账号,账号不符合要求提示,账号不符合要求
+verify_failed,text,登录失败时提示,验证失败
+view,text,通用查看文本,查看
+wait_header,text,我的待办等待中TAB表头,等待中
+wait_to_colock,text,作业执行界面共锁表头,待共锁({0})
+warn,text,通用警告文本,警告
+welcome_tip,text,初始化欢迎界面文本,您好,欢迎您使用
+workflow_already_exists,text,导入流程模式时,流程模式已存在文本提示,流程模式已存在
+workflow_manage_title,text,流程模式管理标题,流程模式管理
+workflow_mode,text,通用流程模式文本,流程模式
+workflow_mode_manage_delete_succeed,text,删除流程模式成功时提示,删除流程模式成功
+workflow_mode_status_update_failed,text,流程模式状态修改失败时提示,状态修改失败
+workflow_mode_status_update_succeed,text,流程模式状态修改成功时提示,状态修改成功
+workflow_name,text,流程模式表头,流程模式名称
+workflow_setting,text,流程模式设置标题,流程设置
+workflow_step_confirm_member,text,流程模式设置项标题,执行确认人员
+workflow_step_confirm_role,text,流程模式设置项标题,执行确认角色
+workflow_step_confirm_type,text,流程模式设置项标题,执行确认方式
+workflow_step_description,text,流程模式设置项标题,步骤操作说明
+workflow_step_function,text,流程模式设置项标题,步骤功能
+workflow_step_title,text,流程模式设置项标题,步骤标题
+workflow_step_title_short,text,流程模式设置项标题,步骤标题缩写
+workstation_already_exists,text,添加区域是检查同级是否已经存在区域,存在时弹窗文本,区域已存在
+workstation_is_in_bottom,text,区域下移时检测是否已经到底部,到底部提示文本,区域已经在底部
+workstation_is_in_top,text,区域上移时检测是否已经到顶部,到顶部提示文本,区域已经在顶部
+workstation_manage_delete_failed,text,区域删除时检查是否有正在使用的作业,有的话提示无法删除,"无法删除区域\""{0}\"""
+workstation_manage_delete_succeed,text,区域删除成功时提示,"删除区域\""{0}\""成功"
+workstation_manage_new_workstation,text,新增区域标题,新增区域
+workstation_manage_title,text,区域管理标题,区域管理
+workstation_manage_workstation_name,text,新增区域显示文本,区域名称
+you_are_not_locker_tip,text,非上锁人执行上锁/解锁作业时提示文本,您不是上锁人,无法执行此操作
+zone,text,首页区域范围,区域范围

+ 20 - 0
app/src/main/java/com/grkj/iscs/ISCSApplication.kt

@@ -16,6 +16,11 @@ import com.grkj.iscs.features.splash.activity.SplashActivity
 import com.grkj.ui_base.service.CheckKeyInfoTask
 import com.grkj.shared.model.EventBean
 import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.shared.utils.i18n.I18nManager
+import com.grkj.shared.utils.i18n.LanguageCatalog
+import com.grkj.shared.utils.i18n.LanguageStore
+import com.grkj.shared.utils.i18n.source.AssetsCsvSource
+import com.grkj.shared.utils.i18n.source.FileCsvSource
 import com.grkj.ui_base.business.ModbusBusinessManager
 import com.grkj.ui_base.config.ISCSConfig
 import com.grkj.ui_base.utils.ble.BleUtil
@@ -37,6 +42,7 @@ import org.greenrobot.eventbus.Subscribe
 import org.greenrobot.eventbus.ThreadMode
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
+import java.util.Locale
 
 
 /**
@@ -66,6 +72,20 @@ class ISCSApplication : Application() {
         if (ISCSConfig.DEBUG) {
             LogUtils.setGlobalLogLevel(Level.DEBUG)
         }
+        I18nManager.init(
+            defaultLocale = LanguageStore.resolveEffectiveLocale(this),
+            initialSources = arrayOf(
+                com.grkj.shared.utils.i18n.source.AssetsCsvSource(this, "i18n", mergedMode = false),
+                com.grkj.shared.utils.i18n.source.FileCsvSource(this, "i18n", mergedMode = false)
+            ),
+            eagerLoad = true
+        )
+
+        // 建议:可提前刷新一下目录(不强制)
+        LanguageCatalog.refresh(this)
+
+        // 若“跟随系统”,监听系统语言变化
+        LanguageStore.registerSystemLocaleObserver(this)
         if (ISCSConfig.isInit) {
             BleUtil.instance?.initBle(this)
         }

+ 1 - 1
app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/AddPointDialog.kt

@@ -99,7 +99,7 @@ class AddPointDialog(
             return false
         }
         if (binding.rfidTagTv.text.isNullOrBlank()) {
-            PopTip.build().tip(R.string.please_select_rfid_tag)
+            PopTip.build().tip(R.string.please_select_rfid_token)
             return false
         }
         return true

+ 1 - 1
app/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/UpdatePointDialog.kt

@@ -120,7 +120,7 @@ class UpdatePointDialog(
             return false
         }
         if (binding.rfidTagTv.tag == null) {
-            PopTip.build().tip(R.string.please_select_rfid_tag)
+            PopTip.build().tip(R.string.please_select_rfid_token)
             return false
         }
         return true

+ 2 - 6
app/src/main/java/com/grkj/iscs/features/main/fragment/home/HomeFragment.kt

@@ -7,9 +7,6 @@ import com.drake.brv.BindingAdapter
 import com.drake.brv.utils.linear
 import com.drake.brv.utils.models
 import com.drake.brv.utils.setup
-import com.google.gson.Gson
-import com.google.gson.reflect.TypeToken
-import com.grkj.data.data.MMKVConstants
 import com.grkj.data.data.MainDomainData
 import com.grkj.data.enums.RoleFunctionalPermissionsEnum
 import com.grkj.iscs.R
@@ -27,7 +24,6 @@ import com.grkj.ui_base.utils.extension.tip
 import com.kongzue.dialogx.dialogs.PopTip
 import com.loper7.date_time_picker.dialog.CardDatePickerDialog
 import com.sik.sikcore.date.TimeUtils
-import com.sik.sikcore.extension.getMMKVData
 import com.sik.sikcore.extension.setDebouncedClickListener
 import dagger.hilt.android.AndroidEntryPoint
 import me.jessyan.autosize.AutoSize
@@ -176,7 +172,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
                         TimeUtils.DEFAULT_DATE_HOUR_MIN_FORMAT
                     )
                 ) {
-                    PopTip.build().tip(R.string.start_time_must_large_then_end_time)
+                    PopTip.build().tip(R.string.end_time_must_large_then_start_time)
                     return@setOnChoose
                 }
             } else {
@@ -186,7 +182,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
                         TimeUtils.DEFAULT_DATE_HOUR_MIN_FORMAT
                     )
                 ) {
-                    PopTip.build().tip(R.string.start_time_must_large_then_end_time)
+                    PopTip.build().tip(R.string.end_time_must_large_then_start_time)
                     return@setOnChoose
                 }
             }

+ 1 - 1
app/src/main/res/layout-land/fragment_job_execute.xml

@@ -417,7 +417,7 @@
                         android:gravity="center"
                         android:minHeight="@dimen/common_btn_height"
                         android:paddingHorizontal="@dimen/common_spacing"
-                        android:text="@string/cancel_the_job"
+                        android:text="@string/cancel_job"
                         android:textColor="@color/white"
                         android:textSize="@dimen/common_btn_text_size"
                         android:visibility="gone" />

+ 1 - 1
app/src/main/res/layout/dialog_add_card.xml

@@ -81,7 +81,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_marginTop="@dimen/common_spacing_2x"
-                android:text="@string/card_nickname"
+                android:text="@string/username"
                 android:textColor="@color/black"
                 android:textSize="@dimen/common_text_size"
                 app:layout_constraintEnd_toEndOf="@+id/card_nfc_tv"

+ 1 - 1
app/src/main/res/layout/dialog_add_point.xml

@@ -195,7 +195,7 @@
                 android:layout_marginLeft="@dimen/common_spacing"
                 android:background="@drawable/bg_common_input"
                 android:drawableRight="@mipmap/icon_drop_down"
-                android:hint="@string/please_select_rfid_tag"
+                android:hint="@string/please_select_rfid_token"
                 android:maxLines="1"
                 android:paddingHorizontal="@dimen/common_spacing"
                 android:paddingVertical="2dp"

+ 1 - 1
app/src/main/res/layout/dialog_filter_card.xml

@@ -81,7 +81,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_marginTop="@dimen/common_spacing_2x"
-                android:text="@string/card_nickname"
+                android:text="@string/username"
                 android:textColor="@color/black"
                 android:textSize="@dimen/common_text_size"
                 app:layout_constraintEnd_toEndOf="@+id/card_nfc_tv"

+ 1 - 1
app/src/main/res/layout/dialog_update_card.xml

@@ -80,7 +80,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_marginTop="@dimen/common_spacing_2x"
-                android:text="@string/card_nickname"
+                android:text="@string/username"
                 android:textColor="@color/black"
                 android:textSize="@dimen/common_text_size"
                 app:layout_constraintEnd_toEndOf="@+id/card_nfc_tv"

+ 1 - 1
app/src/main/res/layout/dialog_update_point.xml

@@ -195,7 +195,7 @@
                 android:layout_marginLeft="@dimen/common_spacing"
                 android:background="@drawable/bg_common_input"
                 android:drawableRight="@mipmap/icon_drop_down"
-                android:hint="@string/please_select_rfid_tag"
+                android:hint="@string/please_select_rfid_token"
                 android:maxLines="1"
                 android:paddingHorizontal="@dimen/common_spacing"
                 android:paddingVertical="2dp"

+ 1 - 1
app/src/main/res/layout/fragment_job_execute.xml

@@ -423,7 +423,7 @@
                         android:gravity="center"
                         android:minHeight="@dimen/common_btn_height"
                         android:paddingHorizontal="@dimen/common_spacing"
-                        android:text="@string/cancel_the_job"
+                        android:text="@string/cancel_job"
                         android:textColor="@color/white"
                         android:textSize="@dimen/common_btn_text_size"
                         android:visibility="gone" />

+ 1 - 3
app/src/main/res/values-en/strings.xml

@@ -141,7 +141,7 @@
     <string name="job_lost">Job lost</string>
     <string name="point_name_tv">Point name</string>
     <string name="please_select_start_time">please select start time first</string>
-    <string name="start_time_must_large_then_end_time">start time must large then end time</string>
+    <string name="end_time_must_large_then_start_time">end time must large then start time</string>
     <string name="manage_role_function_permission">Function permissions</string>
     <string name="expand_collapse">Expand/Collapse</string>
     <string name="all_select_not_all_select">Select All/Select None</string>
@@ -292,7 +292,6 @@
     <string name="card_manage_new_card_title">New card</string>
     <string name="card_nfc">Card NFC</string>
     <string name="please_input_card_nfc">Please input card NFC</string>
-    <string name="card_nickname">Username</string>
     <string name="please_select_card_username">Please select username</string>
 
     <!-- RFID 标签 -->
@@ -310,7 +309,6 @@
     <string name="role_in_preset_tip">The preset role cannot be deleted</string>
     <string name="point_manage_add_title">Add point</string>
     <string name="role_manage_add_title">Add role</string>
-    <string name="please_select_rfid_tag">Please select RFID tag</string>
     <string name="point_manage_rfid">RFID</string>
     <string name="point_manage_update_title">Update point</string>
     <string name="update_point_succeed">Update point success</string>

+ 2 - 4
app/src/main/res/values-zh/strings.xml

@@ -141,14 +141,14 @@
     <string name="job_lost">作业丢失</string>
     <string name="point_name_tv">隔离点</string>
     <string name="please_select_start_time">请先选择开始时间</string>
-    <string name="start_time_must_large_then_end_time">开始时间必须大于结束时间</string>
+    <string name="end_time_must_large_then_start_time">结束时间必须大于开始时间</string>
     <string name="manage_role_function_permission">功能权限</string>
     <string name="expand_collapse">展开/折叠</string>
     <string name="all_select_not_all_select">全选/全不选</string>
     <string name="add_role_succeed">新增角色成功</string>
     <string name="add_role_failed">新增角色失败</string>
     <string name="update_role_succeed">角色更新成功</string>
-    <string name="update_role_failed">角色更新试下</string>
+    <string name="update_role_failed">角色更新失败</string>
     <string name="update_user_succeed">用户更新成功</string>
     <string name="update_user_failed">用户更新失败</string>
     <string name="locked_points_title">锁定中的点位</string>
@@ -292,7 +292,6 @@
     <string name="card_manage_new_card_title">新增卡片</string>
     <string name="card_nfc">卡片 NFC</string>
     <string name="please_input_card_nfc">请输入卡片 NFC</string>
-    <string name="card_nickname">用户名称</string>
     <string name="please_select_card_username">请选择用户名称</string>
 
     <!-- RFID 标签 -->
@@ -310,7 +309,6 @@
     <string name="role_in_preset_tip">预设角色不允许删除</string>
     <string name="point_manage_add_title">添加点位</string>
     <string name="role_manage_add_title">添加角色</string>
-    <string name="please_select_rfid_tag">请选择rfid标签</string>
     <string name="point_manage_rfid">RFID</string>
     <string name="point_manage_update_title">修改点位</string>
     <string name="update_point_succeed">保存点位成功</string>

+ 2 - 4
app/src/main/res/values/strings.xml

@@ -140,14 +140,14 @@
     <string name="job_lost">作业丢失</string>
     <string name="point_name_tv">隔离点</string>
     <string name="please_select_start_time">请先选择开始时间</string>
-    <string name="start_time_must_large_then_end_time">开始时间必须大于结束时间</string>
+    <string name="end_time_must_large_then_start_time">结束时间必须大于开始时间</string>
     <string name="manage_role_function_permission">功能权限</string>
     <string name="expand_collapse">展开/折叠</string>
     <string name="all_select_not_all_select">全选/全不选</string>
     <string name="add_role_succeed">新增角色成功</string>
     <string name="add_role_failed">新增角色失败</string>
     <string name="update_role_succeed">角色更新成功</string>
-    <string name="update_role_failed">角色更新试下</string>
+    <string name="update_role_failed">角色更新失败</string>
     <string name="update_user_succeed">用户更新成功</string>
     <string name="update_user_failed">用户更新失败</string>
     <string name="locked_points_title">锁定中的点位</string>
@@ -295,7 +295,6 @@
     <string name="card_manage_new_card_title">新增卡片</string>
     <string name="card_nfc">卡片 NFC</string>
     <string name="please_input_card_nfc">请输入卡片 NFC</string>
-    <string name="card_nickname">用户名称</string>
     <string name="please_select_card_username">请选择用户名称</string>
 
     <!-- RFID 标签 -->
@@ -313,7 +312,6 @@
     <string name="role_in_preset_tip">预设角色不允许删除</string>
     <string name="point_manage_add_title">添加点位</string>
     <string name="role_manage_add_title">添加角色</string>
-    <string name="please_select_rfid_tag">请选择rfid标签</string>
     <string name="point_manage_rfid">RFID</string>
     <string name="point_manage_update_title">修改点位</string>
     <string name="update_point_succeed">保存点位成功</string>

+ 5 - 0
data/src/main/java/com/grkj/data/data/MMKVConstants.kt

@@ -39,4 +39,9 @@ object MMKVConstants {
      * 锁柜id
      */
     const val KEY_LOCK_CABINET_ID = "key_lock_cabinet_id"
+
+    /**
+     * 语言选择
+     */
+    const val KEY_LOCALE = "key_locale"
 }

+ 8 - 0
shared/build.gradle.kts

@@ -1,6 +1,7 @@
 plugins {
     alias(libs.plugins.android.library)
     alias(libs.plugins.kotlin.android)
+    id("org.jetbrains.kotlin.kapt")
     id("com.google.devtools.ksp")
     id("com.google.dagger.hilt.android")
 }
@@ -43,11 +44,18 @@ android {
             version = "3.22.1"
         }
     }
+    buildFeatures {
+        dataBinding = true
+    }
 }
 
 dependencies {
 
     implementation(libs.androidx.core.ktx)
+    compileOnly(libs.androidx.appcompat)
+    compileOnly(libs.material)
+    compileOnly(libs.androidx.activity)
+    compileOnly(libs.androidx.constraintlayout)
     api(libs.sik.extension.core)
     api(libs.sik.extension.encrypt)
     api(libs.sik.extension.image)

+ 4 - 0
shared/src/main/java/com/grkj/shared/utils/i18n/CsvImporter.kt

@@ -0,0 +1,4 @@
+package com.grkj.shared.utils.i18n
+
+class CsvImporter {
+}

+ 35 - 0
shared/src/main/java/com/grkj/shared/utils/i18n/I18nFormatter.kt

@@ -0,0 +1,35 @@
+package com.grkj.shared.utils.i18n
+
+import java.util.Locale
+import java.util.regex.Pattern
+
+/**
+ * 轻量格式化器
+ *
+ * - 命名占位:{name} / {count} → 先转下标占位,再用 java.text.MessageFormat 处理
+ *   (原因:MessageFormat 对部分语言/数字分组/转义更稳,且性能可接受)
+ * - 性能:仅在存在占位时分配 StringBuilder/数组,绝大多数文本为直接返回。
+ */
+object I18nFormatter {
+    private val namedPattern: Pattern = Pattern.compile("\\{([a-zA-Z_][a-zA-Z0-9_]*)}")
+
+    fun format(pattern: String, args: Map<String, Any?>?, locale: Locale): String {
+        if (args.isNullOrEmpty() || !pattern.contains('{')) return pattern
+        // 命名转下标
+        val order = ArrayList<String>(args.size)
+        val sb = StringBuilder(pattern.length + 8)
+        val m = namedPattern.matcher(pattern)
+        var last = 0
+        while (m.find()) {
+            sb.append(pattern, last, m.start())
+            val name = m.group(1)
+            var idx = order.indexOf(name)
+            if (idx == -1) { order.add(name); idx = order.size - 1 }
+            sb.append('{').append(idx).append('}')
+            last = m.end()
+        }
+        sb.append(pattern, last, pattern.length)
+        val values = Array(order.size) { i -> args[order[i]] }
+        return java.text.MessageFormat(sb.toString(), locale).format(values)
+    }
+}

+ 148 - 0
shared/src/main/java/com/grkj/shared/utils/i18n/I18nManager.kt

@@ -0,0 +1,148 @@
+package com.grkj.shared.utils.i18n
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import java.util.Locale
+import java.util.concurrent.Executors
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * I18nManager
+ *
+ * - 负责:当前 Locale、词条查询、热切换、数据源聚合与(重)加载。
+ * - 性能:所有加载/切换在单线程执行器(串行)中完成;查词为 O(1) 内存 Map。
+ * - 线程安全:词典使用 AtomicReference 原子替换,无需加锁读。
+ *
+ * 使用:
+ *  1) Application.onCreate() 调用 [init],提供默认 Locale 与初始数据源。
+ *  2) UI 查询用 [t];切换语言调用 [setLocale]。
+ *  3) 导入后调用 [reload] 或 [addSource],自动刷新观察者(DataBinding 已接上)。
+ */
+object I18nManager {
+
+    /** 当前生效的 Locale(热切换会更新) */
+    private val _locale = MutableStateFlow(Locale.getDefault())
+    val locale: StateFlow<Locale> = _locale
+
+    /** 词典缓存(key -> I18nEntry),原子替换,读路径零锁 */
+    private val dictRef = AtomicReference<Map<String, I18nEntry>>(emptyMap())
+
+    /** 已注册的数据源(后注册者覆盖先注册者) */
+    private val sources = mutableListOf<I18nSource>()
+
+    /** 串行后台执行器:避免并发构建词典产生抖动 */
+    private val singleExecutor = Executors.newSingleThreadExecutor { r ->
+        Thread(r, "i18n-single").apply { isDaemon = true }
+    }
+    private val bg = CoroutineScope(singleExecutor.asCoroutineDispatcher())
+    private val main = CoroutineScope(Dispatchers.Main.immediate)
+    private var currentReloadJob: Job? = null
+
+    /**
+     * 初始化框架(建议在 Application.onCreate 调用)
+     *
+     * @param defaultLocale 启动默认语言
+     * @param initialSources 初始数据源(如内置 assets、可写 files)
+     * @param eagerLoad 是否阻塞主线程同步加载首个词典(默认 true)
+     *        若你追求极致冷启动,可设为 false:主线程只设定 locale,后台构建;UI 首帧用 key 或 fallback,随后自动刷新。
+     */
+    @JvmStatic
+    fun init(
+        defaultLocale: Locale,
+        vararg initialSources: I18nSource,
+        eagerLoad: Boolean = true
+    ) {
+        _locale.value = defaultLocale
+        sources.clear()
+        sources.addAll(initialSources)
+
+        if (eagerLoad) {
+            // 同步加载,保证首帧即有文案
+            dictRef.set(I18nRepository.build(sources, defaultLocale))
+        } else {
+            // 后台加载,首帧可降级,完成后触发一次刷新
+            dictRef.set(emptyMap())
+            reload()
+        }
+    }
+
+    /**
+     * 注册新增数据源(例如:导入后将 FileCsvSource 放在最后覆盖)
+     * @param source 数据源
+     * @param refresh 是否立即重建词典(默认 true)
+     */
+    @JvmStatic
+    fun addSource(source: I18nSource, refresh: Boolean = true) {
+        sources.add(source)
+        if (refresh) reload()
+    }
+
+    /**
+     * 重新按当前 locale 构建词典。
+     * - 去重策略:后注册数据源覆盖先注册数据源的同名 key。
+     * - 原子替换:构建完成后一次性替换 dictRef,并在主线程发出 locale 的“自刷新”通知。
+     */
+    @JvmStatic
+    fun reload() {
+        currentReloadJob?.cancel()
+        val targetLocale = _locale.value
+        currentReloadJob = bg.launch {
+            val built = I18nRepository.build(sources, targetLocale)
+            dictRef.set(built)
+            // 触发绑定刷新:重复设置相同值也会触发 collectLatest
+            main.launch { _locale.value = targetLocale }
+        }
+    }
+
+    /**
+     * 设置新的 Locale 并重建词典。
+     * - 切换速度:只重建一次 Map;UI 无需重启 Activity。
+     */
+    @JvmStatic
+    fun setLocale(locale: Locale) {
+        if (locale == _locale.value) return
+        _locale.value = locale // 先更新流,便于 UI 立刻拿到新 Locale(文本随后到)
+        reload()
+    }
+
+    /**
+     * 翻译主入口:根据 key 返回文本。
+     *
+     * @param key 键名(建议命名空间:模块.页面.含义)
+     * @param args 命名占位({name}、{count}),自动转下标填充
+     * @param count 可选复数选择辅助(如 items.count)
+     * @param selectKey 可选选择分支(如 gender:male/female/other)
+     * @param fallback 兜底文本(缺 key 时使用;默认返回 key)
+     */
+    @JvmStatic
+    fun t(
+        key: String,
+        args: Map<String, Any?>? = null,
+        count: Number? = null,
+        selectKey: String? = null,
+        fallback: String? = null
+    ): String {
+        val entry = dictRef.get()[key] ?: return fallback ?: key
+        return when (entry.type) {
+            I18nType.TEXT -> I18nFormatter.format(entry.value ?: fallback ?: key, args, _locale.value)
+            I18nType.PLURAL -> {
+                val n = count ?: 0
+                val form = PluralRules.pick(_locale.value, n)
+                val pattern = entry.plurals[form] ?: entry.plurals["other"]
+                ?: entry.value ?: fallback ?: key
+                I18nFormatter.format(pattern, (args ?: emptyMap()) + mapOf("count" to n), _locale.value)
+            }
+            I18nType.SELECT -> {
+                val sel = (selectKey ?: "other").lowercase()
+                val pattern = entry.selects[sel] ?: entry.selects["other"]
+                ?: entry.value ?: fallback ?: key
+                I18nFormatter.format(pattern, args, _locale.value)
+            }
+        }
+    }
+}

+ 41 - 0
shared/src/main/java/com/grkj/shared/utils/i18n/I18nRepository.kt

@@ -0,0 +1,41 @@
+package com.grkj.shared.utils.i18n
+
+import java.util.Locale
+
+/**
+ * I18nRepository
+ *
+ * - 静态构建器:聚合所有数据源,生成最终词典 Map。
+ * - 合并规则:按 sources 顺序,后者覆盖先者(简洁明了,便于“外部导入覆盖内置”)。
+ */
+object I18nRepository {
+
+    /**
+     * 构建最终词典。
+     * @param sources 数据源(按注册顺序)
+     * @param locale 目标 Locale
+     */
+    fun build(sources: List<I18nSource>, locale: Locale): Map<String, I18nEntry> {
+        if (sources.isEmpty()) return emptyMap()
+        val result = LinkedHashMap<String, I18nEntry>(4096) // 预估容量,减少扩容
+        for (src in sources) {
+            val block = src.load(locale) // IO/解析由 Source 自担
+            if (block.isNotEmpty()) {
+                // 后覆盖先
+                for ((k, v) in block) result[k] = v
+            }
+        }
+        return result
+    }
+}
+
+/** 词条实体(TEXT/PLURAL/SELECT 三类型统一承载) */
+data class I18nEntry(
+    val key: String,
+    val type: I18nType,
+    val value: String? = null,                       // 直接文本或默认
+    val plurals: Map<String, String> = emptyMap(),   // one/other/…
+    val selects: Map<String, String> = emptyMap()    // male/female/other/…
+)
+
+enum class I18nType { TEXT, PLURAL, SELECT }

+ 12 - 0
shared/src/main/java/com/grkj/shared/utils/i18n/I18nSource.kt

@@ -0,0 +1,12 @@
+package com.grkj.shared.utils.i18n
+
+import java.util.Locale
+
+/**
+ * 数据源抽象。
+ * - 推荐实现:AssetsCsvSource / FileCsvSource / MemorySource(测试用)
+ * - 语义:给定 Locale,返回 key->entry 映射(同 key 后覆盖)。
+ */
+fun interface I18nSource {
+    fun load(locale: Locale): Map<String, I18nEntry>
+}

+ 218 - 0
shared/src/main/java/com/grkj/shared/utils/i18n/LanguageCatalog.kt

@@ -0,0 +1,218 @@
+package com.grkj.shared.utils.i18n
+
+import android.content.Context
+import android.content.res.AssetManager
+import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import java.io.*
+import java.util.*
+import java.util.concurrent.Executors
+
+/**
+ * 语言目录(含显示名):
+ * - 单语文件:<locale>.csv
+ * - 合表:all.csv(表头含 localeTag)
+ * - 显示名:优先读 key=i18n.language_name;否则 fallback 为 locale 的自称
+ * - 去重:files 覆盖 assets
+ */
+object LanguageCatalog {
+
+    enum class Source { ASSETS, FILES }
+
+    data class LanguageItem(
+        val tag: String,
+        val locale: Locale,
+        val source: Source,
+        val displayName: String
+    )
+
+    private const val ASSETS_DIR = "i18n"
+    private const val FILES_DIR = "i18n"
+    private const val MERGED_FILE = "all.csv"
+    private const val NAME_KEY = "i18n.language_name"
+
+    private val bg = Executors.newSingleThreadExecutor { r -> Thread(r, "lang-catalog").apply { isDaemon = true } }
+        .asCoroutineDispatcher()
+    private val _items = MutableStateFlow<List<LanguageItem>>(emptyList())
+    val items: StateFlow<List<LanguageItem>> = _items
+
+    /** 刷新目录(扫描 + 读取显示名 + 统一排序) */
+    fun refresh(context: Context) {
+        CoroutineScope(bg).launch {
+            val files = scanFiles(context)
+            val assets = scanAssets(context.assets)
+
+            // 去重(files 覆盖 assets)
+            val map = LinkedHashMap<String, LanguageItem>(assets.size + files.size)
+            for (it in assets) map[it.tag] = it
+            for (it in files) map[it.tag] = it
+
+            val list = map.values.toMutableList()
+
+            // 排序:跟随系统由设置页控制。这里按 displayName 升序。
+            list.sortBy { it.displayName.lowercase(it.locale) }
+
+            withContext(Dispatchers.Main) { _items.value = list }
+        }
+    }
+
+    fun onImported(context: Context) = refresh(context)
+
+    // ===== 扫描 assets =====
+    private fun scanAssets(am: AssetManager): List<LanguageItem> {
+        val out = ArrayList<LanguageItem>(8)
+        val names = runCatching { am.list(ASSETS_DIR)?.toList().orEmpty() }.getOrDefault(emptyList())
+
+        // 单语
+        names.filter { it.endsWith(".csv", true) && !it.equals(MERGED_FILE, true) }
+            .mapNotNull { fn ->
+                val tag = fn.removeSuffix(".csv")
+                if (!isValidTag(tag)) return@mapNotNull null
+                val dn = readDisplayNameFromSingle { am.open("$ASSETS_DIR/$fn") } ?: endonym(tag)
+                LanguageItem(tag, Locale.forLanguageTag(tag), Source.ASSETS, dn)
+            }
+            .also { out += it }
+
+        // 合表
+        if (names.any { it.equals(MERGED_FILE, true) }) {
+            runCatching {
+                am.open("$ASSETS_DIR/$MERGED_FILE").use { input ->
+                    val tags = readTagsFromMergedHeader(input)
+                    for (tag in tags) {
+                        val dn = readDisplayNameFromMergedColumn { am.open("$ASSETS_DIR/$MERGED_FILE") }[tag]
+                            ?: endonym(tag)
+                        out += LanguageItem(tag, Locale.forLanguageTag(tag), Source.ASSETS, dn)
+                    }
+                }
+            }
+        }
+        return out
+    }
+
+    // ===== 扫描 files =====
+    private fun scanFiles(context: Context): List<LanguageItem> {
+        val out = ArrayList<LanguageItem>(8)
+        val dir = File(context.filesDir, FILES_DIR).apply { mkdirs() }
+        val files = dir.listFiles()?.filter { it.isFile && it.name.endsWith(".csv", true) }.orEmpty()
+
+        // 单语
+        files.filter { !it.name.equals(MERGED_FILE, true) }.forEach { f ->
+            val tag = f.name.removeSuffix(".csv")
+            if (isValidTag(tag)) {
+                val dn = readDisplayNameFromSingle { FileInputStream(f) } ?: endonym(tag)
+                out += LanguageItem(tag, Locale.forLanguageTag(tag), Source.FILES, dn)
+            }
+        }
+
+        // 合表
+        files.firstOrNull { it.name.equals(MERGED_FILE, true) }?.let { merged ->
+            runCatching {
+                FileInputStream(merged).use { fis ->
+                    val tags = readTagsFromMergedHeader(fis)
+                    val names = readDisplayNameFromMergedColumn { FileInputStream(merged) }
+                    for (tag in tags) {
+                        val dn = names[tag] ?: endonym(tag)
+                        out += LanguageItem(tag, Locale.forLanguageTag(tag), Source.FILES, dn)
+                    }
+                }
+            }
+        }
+        return out
+    }
+
+    // ===== 读取显示名:单语 CSV =====
+    private fun readDisplayNameFromSingle(open: () -> InputStream): String? = runCatching {
+        open().use { input ->
+            BufferedReader(InputStreamReader(input)).use { br ->
+                val header = br.readLine() ?: return null
+                val cols = splitCsv(header)
+                val iKey = cols.indexOf("key")
+                val iType = cols.indexOf("type")
+                val iVal = cols.indexOf("value")
+                if (iKey < 0 || iVal < 0) return null
+                var line: String?
+                while (true) {
+                    line = br.readLine() ?: break
+                    if (line.isNullOrBlank()) continue
+                    val c = splitCsv(line!!)
+                    if (c.getOrNull(iKey)?.trim() == NAME_KEY) {
+                        return c.getOrNull(iVal)?.trim()?.takeIf { it.isNotEmpty() }
+                    }
+                }
+                null
+            }
+        }
+    }.getOrNull()
+
+    // ===== 读取显示名:合表 CSV =====
+    private fun readDisplayNameFromMergedColumn(open: () -> InputStream): Map<String, String> = runCatching {
+        open().use { input ->
+            BufferedReader(InputStreamReader(input)).use { br ->
+                val header = br.readLine() ?: return emptyMap()
+                val head = splitCsv(header)
+                val fixed = setOf("key", "type", "comment")
+                val localeIdx = head.mapIndexedNotNull { idx, name -> if (name !in fixed) name to idx else null }.toMap()
+                val iKey = head.indexOf("key")
+                if (iKey < 0) return emptyMap()
+
+                val out = HashMap<String, String>(localeIdx.size)
+                var line: String?
+                while (true) {
+                    line = br.readLine() ?: break
+                    if (line.isNullOrBlank()) continue
+                    val c = splitCsv(line!!)
+                    if (c.getOrNull(iKey)?.trim() == NAME_KEY) {
+                        for ((tag, i) in localeIdx) {
+                            c.getOrNull(i)?.trim()?.takeIf { it.isNotEmpty() }?.let { out[tag] = it }
+                        }
+                        break
+                    }
+                }
+                out
+            }
+        }
+    }.getOrDefault(emptyMap())
+
+    // ===== 读取合表表头的 locale 列 =====
+    private fun readTagsFromMergedHeader(input: InputStream): List<String> {
+        input.use {
+            BufferedReader(InputStreamReader(it)).use { br ->
+                val header = br.readLine() ?: return emptyList()
+                val cols = splitCsv(header)
+                val fixed = setOf("key", "type", "comment")
+                return cols.filter { it !in fixed && isValidTag(it) }
+            }
+        }
+    }
+
+    // ===== 简易 CSV Split(支持引号/逗号/双引号转义) =====
+    private fun splitCsv(line: String): List<String> {
+        val out = ArrayList<String>(8)
+        val sb = StringBuilder()
+        var i = 0
+        var inQ = false
+        while (i < line.length) {
+            val c = line[i]
+            when {
+                c == '"' -> {
+                    if (inQ && i + 1 < line.length && line[i + 1] == '"') { sb.append('"'); i++ } else inQ = !inQ
+                }
+                c == ',' && !inQ -> { out.add(sb.toString()); sb.setLength(0) }
+                else -> sb.append(c)
+            }
+            i++
+        }
+        out.add(sb.toString())
+        return out.map { it.trim() }
+    }
+
+    private fun isValidTag(tag: String): Boolean =
+        tag.matches(Regex("^[a-zA-Z]{2,8}(-[a-zA-Z0-9]{2,8})*$"))
+
+    private fun endonym(tag: String): String {
+        val loc = Locale.forLanguageTag(tag)
+        val self = loc.getDisplayName(loc).replaceFirstChar { it.uppercase(loc) }
+        return self.ifEmpty { tag }
+    }
+}

+ 98 - 0
shared/src/main/java/com/grkj/shared/utils/i18n/LanguageStore.kt

@@ -0,0 +1,98 @@
+package com.grkj.shared.utils.i18n
+
+import android.app.Application
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Build
+import java.util.Locale
+import com.tencent.mmkv.MMKV
+
+/**
+ * 使用 MMKV 持久化“语言选择”:
+ * - 模式:跟随系统 / 显式指定某语言(可扩展多国语言)
+ * - 冷启动恢复:resolveEffectiveLocale()
+ * - 跟随系统:监听系统语言变更,实时调用 I18nManager.setLocale()
+ */
+object LanguageStore {
+
+    // === MMKV 持久化键 ===
+    private const val KV_ID = "i18n_prefs"
+    private const val KEY_MODE = "language_mode"          // "follow" | "explicit"
+    private const val KEY_LOCALE_TAG = "explicit_locale"  // 例如 "zh-CN" / "en-US"
+
+    // === 对外枚举:可扩展更多语言 ===
+    enum class Mode { FOLLOW_SYSTEM, EXPLICIT }
+
+    enum class PresetLocale(val tag: String) {
+        ZH_CN("zh-CN"),
+        EN_US("en-US");
+        // 你要多国语言就在此处追加:JA_JP("ja-JP"), ES_ES("es-ES") 之类
+        fun toLocale(): Locale = Locale.forLanguageTag(tag)
+        companion object {
+            fun fromTag(tag: String?): PresetLocale? = values().firstOrNull { it.tag == tag }
+        }
+    }
+
+    private fun kv() = MMKV.mmkvWithID(KV_ID, MMKV.SINGLE_PROCESS_MODE)
+
+    /** 读取当前模式 */
+    fun currentMode(): Mode = when (kv().getString(KEY_MODE, "follow")) {
+        "explicit" -> Mode.EXPLICIT
+        else -> Mode.FOLLOW_SYSTEM
+    }
+
+    /** 读取显式语言(可能为 null) */
+    fun currentExplicitLocale(): Locale? =
+        kv().getString(KEY_LOCALE_TAG, null)?.let { Locale.forLanguageTag(it) }
+
+    /** 设置为“跟随系统”,并立即让 I18n 生效 */
+    fun setFollowSystem(context: Context) {
+        kv().putString(KEY_MODE, "follow")
+        val sys = systemLocale(context)
+        I18nManager.setLocale(sys)
+    }
+
+    /** 设置为“指定语言”,并立即让 I18n 生效 */
+    fun setExplicit(locale: Locale) {
+        kv().putString(KEY_MODE, "explicit")
+        kv().putString(KEY_LOCALE_TAG, locale.toLanguageTag())
+        I18nManager.setLocale(locale)
+    }
+
+    /** 启动时恢复“有效 Locale”:优先显式,其次系统 */
+    fun resolveEffectiveLocale(context: Context): Locale {
+        return when (currentMode()) {
+            Mode.EXPLICIT -> currentExplicitLocale() ?: systemLocale(context)
+            Mode.FOLLOW_SYSTEM -> systemLocale(context)
+        }
+    }
+
+    /** 注册系统语言变化监听(仅跟随系统时会触发 I18nManager.setLocale) */
+    fun registerSystemLocaleObserver(app: Application) {
+        val receiver = object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                if (intent.action == Intent.ACTION_LOCALE_CHANGED) {
+                    if (currentMode() == Mode.FOLLOW_SYSTEM) {
+                        I18nManager.setLocale(systemLocale(context))
+                    }
+                }
+            }
+        }
+        // Android 13+ 的 per-app language 是独立 API,这里仍监听系统 Locale 改变
+        val filter = IntentFilter(Intent.ACTION_LOCALE_CHANGED)
+        app.registerReceiver(receiver, filter)
+        // 你也可以把 receiver 保存起来,在 Application.onTerminate() 里反注册
+    }
+
+    /** 获取系统当前 Locale(API 向后兼容) */
+    fun systemLocale(context: Context): Locale {
+        val cfg = context.resources.configuration
+        return if (Build.VERSION.SDK_INT >= 24) {
+            cfg.locales[0]
+        } else {
+            @Suppress("DEPRECATION") cfg.locale
+        }
+    }
+}

+ 21 - 0
shared/src/main/java/com/grkj/shared/utils/i18n/PluralRules.kt

@@ -0,0 +1,21 @@
+package com.grkj.shared.utils.i18n
+
+import java.util.Locale
+import kotlin.math.abs
+
+/**
+ * 复数规则(最小可用集)
+ * - 如需更完整的 CLDR 规则,可后续切换到 ICU4J;当前实现覆盖常用需求,中文固定 other。
+ */
+object PluralRules {
+    fun pick(locale: Locale, count: Number): String {
+        val n = abs(count.toDouble())
+        return when (locale.language.lowercase(Locale.ROOT)) {
+            "en" -> if (n == 1.0) "one" else "other"
+            "zh" -> "other"
+            "fr" -> if (n == 0.0 || n == 1.0) "one" else "other"
+            // 可按需扩展更多语言
+            else -> "other"
+        }
+    }
+}

+ 57 - 0
shared/src/main/java/com/grkj/shared/utils/i18n/databinding/I18nBindingAdapters.kt

@@ -0,0 +1,57 @@
+package com.grkj.shared.utils.i18n.databinding
+
+import android.view.View
+import android.widget.TextView
+import androidx.appcompat.widget.Toolbar
+import androidx.databinding.BindingAdapter
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.textfield.TextInputLayout
+import com.grkj.shared.utils.i18n.I18nManager
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+/**
+ * 将一个订阅 Job 挂在 View 的 tag 上,随生命周期清理,防止泄漏。
+ * - 为避免 ID 冲突,请在你的 res/values/ids.xml 添加 i18n_locale_observer_job。
+ */
+private fun View.observeLocale(onChange: () -> Unit) {
+    val owner = findViewTreeLifecycleOwner() ?: return
+    val keyId = resources.getIdentifier("i18n_locale_observer_job", "id", context.packageName)
+    (getTag(keyId) as? Job)?.cancel()
+    val job = owner.lifecycleScope.launch {
+        I18nManager.locale.collectLatest { onChange() }
+    }
+    setTag(keyId, job)
+}
+
+@BindingAdapter(value = ["i18nKey", "i18nArgs", "i18nCount", "i18nSelect", "i18nFallback"], requireAll = false)
+fun TextView.bindI18nText(
+    key: String?,
+    args: Map<String, Any?>? = null,
+    count: Number? = null,
+    select: String? = null,
+    fallback: String? = null
+) {
+    if (key.isNullOrBlank()) return
+    val apply = { text = I18nManager.t(key, args, count, select, fallback) }
+    apply()
+    observeLocale { apply() }
+}
+
+@BindingAdapter(value = ["i18nHintKey", "i18nHintArgs"], requireAll = false)
+fun TextInputLayout.bindI18nHint(key: String?, args: Map<String, Any?>? = null) {
+    if (key.isNullOrBlank()) return
+    val apply = { hint = I18nManager.t(key, args) }
+    apply()
+    observeLocale { apply() }
+}
+
+@BindingAdapter(value = ["i18nTitleKey", "i18nTitleArgs"], requireAll = false)
+fun Toolbar.bindI18nTitle(key: String?, args: Map<String, Any?>? = null) {
+    if (key.isNullOrBlank()) return
+    val apply = { title = I18nManager.t(key, args) }
+    apply()
+    observeLocale { apply() }
+}

+ 83 - 0
shared/src/main/java/com/grkj/shared/utils/i18n/importer/CsvImporter.kt

@@ -0,0 +1,83 @@
+package com.grkj.shared.utils.i18n.importer
+
+import android.content.Context
+import com.grkj.shared.utils.i18n.I18nManager
+import com.grkj.shared.utils.i18n.I18nSource
+import com.grkj.shared.utils.i18n.source.FileCsvSource
+import java.io.File
+import java.io.InputStream
+import java.util.Locale
+import java.util.zip.ZipInputStream
+
+/**
+ * CSV/ZIP 导入器
+ * - 目标目录:context.filesDir / dirName
+ * - 策略:
+ *   REPLACE       => 用新文件替换旧文件
+ *   UPSERT        => 新 key 覆盖/追加,旧 key 保留(按 key 合并)
+ *   KEEP_EXISTING => 仅补全旧文件中缺失的 key
+ */
+class CsvImporter(
+    private val context: Context,
+    private val dirName: String = "i18n"
+) {
+
+    enum class MergeStrategy { REPLACE, UPSERT, KEEP_EXISTING }
+
+    private fun dir(): File = File(context.filesDir, dirName).apply { mkdirs() }
+
+    /** 导入单个 CSV(建议文件名 localeTag.csv,例如 zh-CN.csv) */
+    fun importCsv(locale: Locale, input: InputStream, strategy: MergeStrategy): File {
+        val out = File(dir(), "${locale.toLanguageTag()}.csv")
+        val tmp = File(out.parentFile, out.name + ".tmp")
+        tmp.outputStream().use { input.copyTo(it) }
+        applyMerge(out, tmp, strategy)
+        tmp.delete()
+        refreshSource()
+        return out
+    }
+
+    /**
+     * 导入 ZIP:可包含多个 csv(例如 zh-CN.csv / en-US.csv / all.csv)
+     * - 若同时存在 all.csv 与单语表,建议保留单语表方案(查找更快)。
+     */
+    fun importZip(zip: InputStream, strategy: MergeStrategy): List<File> {
+        val outputs = mutableListOf<File>()
+        ZipInputStream(zip).use { zis ->
+            var e = zis.nextEntry
+            while (e != null) {
+                if (!e.isDirectory && e.name.endsWith(".csv", true)) {
+                    val name = e.name.substringAfterLast('/')
+                    val out = File(dir(), name)
+                    val tmp = File(out.parentFile, "$name.tmp")
+                    tmp.outputStream().use { zis.copyTo(it) }
+                    applyMerge(out, tmp, strategy)
+                    tmp.delete()
+                    outputs.add(out)
+                }
+                e = zis.nextEntry
+            }
+        }
+        refreshSource()
+        return outputs
+    }
+
+    /** 导入完成后,将 FileCsvSource 置于 sources 列表末尾并 reload(覆盖内置) */
+    private fun refreshSource() {
+        // 避免重复添加多个 FileCsvSource:允许调用方自行管理;此处简单追加一次也可。
+        I18nManager.addSource(fileSource(), refresh = true)
+    }
+
+    private fun fileSource(): I18nSource = FileCsvSource(context, dirName = dirName, mergedMode = false)
+
+    /** 将 tmp 合并到 target(按 strategy) */
+    private fun applyMerge(target: File, incoming: File, strategy: MergeStrategy) {
+        if (!target.exists() || strategy == MergeStrategy.REPLACE) {
+            incoming.copyTo(target, overwrite = true); return
+        }
+        val oldCsv = target.readText()
+        val newCsv = incoming.readText()
+        val merged = CsvMerger.merge(oldCsv, newCsv, strategy)
+        target.writeText(merged)
+    }
+}

+ 84 - 0
shared/src/main/java/com/grkj/shared/utils/i18n/importer/CsvMerger.kt

@@ -0,0 +1,84 @@
+package com.grkj.shared.utils.i18n.importer
+
+import java.io.BufferedReader
+import java.io.StringReader
+import java.lang.StringBuilder
+
+/**
+ * CSV 合并器(单语模式:key,type,comment,value)
+ * - 依据 key 合并,保留表头。
+ * - 注意:解析器做了近似处理;生产环境强需求可与 CsvUtils 共享 parser。
+ */
+object CsvMerger {
+
+    data class Row(val key: String, val type: String, val comment: String, val value: String)
+
+    fun merge(oldCsv: String, newCsv: String, strategy: CsvImporter.MergeStrategy): String {
+        val old = read(oldCsv).associateBy { it.key }.toMutableMap()
+        val inc = read(newCsv)
+        when (strategy) {
+            CsvImporter.MergeStrategy.REPLACE -> return newCsv
+            CsvImporter.MergeStrategy.UPSERT -> {
+                for (r in inc) old[r.key] = r
+            }
+            CsvImporter.MergeStrategy.KEEP_EXISTING -> {
+                for (r in inc) old.putIfAbsent(r.key, r)
+            }
+        }
+        val sb = StringBuilder(oldCsv.length + newCsv.length / 4)
+        sb.append("key,type,comment,value\n")
+        for ((_, v) in old) {
+            sb.append(escape(v.key)).append(',')
+                .append(escape(v.type)).append(',')
+                .append(escape(v.comment)).append(',')
+                .append(escape(v.value)).append('\n')
+        }
+        return sb.toString()
+    }
+
+    private fun read(csv: String): List<Row> {
+        val br = BufferedReader(StringReader(csv))
+        val header = br.readLine() ?: return emptyList()
+        val cols = header.split(',')
+        val res = ArrayList<Row>(1024)
+        br.lineSequence().forEach { line ->
+            if (line.isBlank()) return@forEach
+            val p = splitCsv(line)
+            val row = Row(
+                key = p.getOrElse(0) { "" },
+                type = p.getOrElse(1) { "text" },
+                comment = p.getOrElse(2) { "" },
+                value = p.getOrElse(3) { "" }
+            )
+            if (row.key.isNotBlank()) res.add(row)
+        }
+        return res
+    }
+
+    private fun splitCsv(line: String): List<String> {
+        val out = ArrayList<String>(4)
+        val sb = StringBuilder()
+        var i = 0
+        var inQ = false
+        while (i < line.length) {
+            val c = line[i]
+            when {
+                c == '"' -> {
+                    if (inQ && i + 1 < line.length && line[i + 1] == '"') { sb.append('"'); i++ }
+                    else inQ = !inQ
+                }
+                c == ',' && !inQ -> { out.add(sb.toString()); sb.setLength(0) }
+                else -> sb.append(c)
+            }
+            i++
+        }
+        out.add(sb.toString())
+        return out
+    }
+
+    private fun escape(s: String): String {
+        val need = s.indexOf('"') >= 0 || s.indexOf(',') >= 0 || s.indexOf('\n') >= 0
+        if (!need) return s
+        return "\"" + s.replace("\"", "\"\"") + "\""
+    }
+}

+ 45 - 0
shared/src/main/java/com/grkj/shared/utils/i18n/source/AssetsCsvSource.kt

@@ -0,0 +1,45 @@
+package com.grkj.shared.utils.i18n.source
+
+import android.content.Context
+import com.grkj.shared.utils.i18n.I18nEntry
+import com.grkj.shared.utils.i18n.I18nSource
+import com.grkj.shared.utils.i18n.util.CsvUtils
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.util.Locale
+
+/**
+ * 从 assets 目录读取 CSV 词库。
+ *
+ * @param dir assets 子目录(默认 "i18n")
+ * @param mergedMode false=推荐:一语言一表(en-US.csv);true=多语合表(all.csv)
+ * @param mergedFileName 合表文件名(仅 mergedMode=true 时生效)
+ */
+class AssetsCsvSource(
+    private val context: Context,
+    private val dir: String = "i18n",
+    private val mergedMode: Boolean = false,
+    private val mergedFileName: String = "all.csv"
+) : I18nSource {
+
+    override fun load(locale: Locale): Map<String, I18nEntry> {
+        return if (!mergedMode) {
+            val name = "${locale.toLanguageTag()}.csv"
+            readSingle("$dir/$name")
+        } else {
+            readMerged("$dir/$mergedFileName", locale)
+        }
+    }
+
+    private fun readSingle(path: String): Map<String, I18nEntry> = runCatching {
+        context.assets.open(path).use { input ->
+            BufferedReader(InputStreamReader(input)).use { CsvUtils.parseSingleLang(it) }
+        }
+    }.getOrElse { emptyMap() }
+
+    private fun readMerged(path: String, locale: Locale): Map<String, I18nEntry> = runCatching {
+        context.assets.open(path).use { input ->
+            BufferedReader(InputStreamReader(input)).use { CsvUtils.parseMerged(it, locale) }
+        }
+    }.getOrElse { emptyMap() }
+}

+ 38 - 0
shared/src/main/java/com/grkj/shared/utils/i18n/source/FileCsvSource.kt

@@ -0,0 +1,38 @@
+package com.grkj.shared.utils.i18n.source
+
+import android.content.Context
+import com.grkj.shared.utils.i18n.I18nEntry
+import com.grkj.shared.utils.i18n.I18nSource
+import com.grkj.shared.utils.i18n.util.CsvUtils
+import java.io.BufferedReader
+import java.io.File
+import java.io.FileReader
+import java.util.Locale
+
+/**
+ * 从 app 私有 files 目录读取 CSV(导入产物)。
+ *
+ * 目录结构(默认 dirName="i18n"):
+ *  - 单语:files/i18n/zh-CN.csv / en-US.csv
+ *  - 合表:files/i18n/all.csv
+ */
+class FileCsvSource(
+    private val context: Context,
+    private val dirName: String = "i18n",
+    private val mergedMode: Boolean = false,
+    private val mergedFileName: String = "all.csv"
+) : I18nSource {
+
+    override fun load(locale: Locale): Map<String, I18nEntry> {
+        val dir = File(context.filesDir, dirName).apply { mkdirs() }
+        return if (!mergedMode) {
+            val f = File(dir, "${locale.toLanguageTag()}.csv")
+            if (f.exists()) BufferedReader(FileReader(f)).use { CsvUtils.parseSingleLang(it) }
+            else emptyMap()
+        } else {
+            val f = File(dir, mergedFileName)
+            if (f.exists()) BufferedReader(FileReader(f)).use { CsvUtils.parseMerged(it, locale) }
+            else emptyMap()
+        }
+    }
+}

+ 116 - 0
shared/src/main/java/com/grkj/shared/utils/i18n/util/CsvUtils.kt

@@ -0,0 +1,116 @@
+package com.grkj.shared.utils.i18n.util
+
+import com.grkj.shared.utils.i18n.I18nEntry
+import com.grkj.shared.utils.i18n.I18nType
+import java.io.BufferedReader
+import java.util.Locale
+
+/**
+ * 轻量 CSV 解析(近 RFC4180)
+ * - 支持引号、双引号转义、逗号/换行
+ * - 倾向一次 pass,少分配
+ * - 表头要求:
+ *   单语模式:key,type,comment,value
+ *   合表模式:key,type,comment,<localeTag...>
+ */
+object CsvUtils {
+
+    private fun parseLine(line: String): List<String> {
+        val out = ArrayList<String>(8)
+        val sb = StringBuilder(line.length + 4)
+        var i = 0
+        var inQ = false
+        while (i < line.length) {
+            val c = line[i]
+            when {
+                c == '"' -> {
+                    if (inQ && i + 1 < line.length && line[i + 1] == '"') {
+                        sb.append('"'); i++
+                    } else inQ = !inQ
+                }
+                c == ',' && !inQ -> {
+                    out.add(sb.toString()); sb.setLength(0)
+                }
+                else -> sb.append(c)
+            }
+            i++
+        }
+        out.add(sb.toString())
+        return out
+    }
+
+    /** 单语 CSV:key,type,comment,value */
+    fun parseSingleLang(br: BufferedReader): Map<String, I18nEntry> {
+        val header = parseLine(br.readLine() ?: return emptyMap())
+        val idxKey = header.indexOf("key")
+        val idxType = header.indexOf("type")
+        val idxVal = header.indexOf("value")
+        if (idxKey < 0 || idxType < 0 || idxVal < 0) return emptyMap()
+
+        val map = LinkedHashMap<String, I18nEntry>(1024)
+        br.lineSequence().forEach { raw ->
+            if (raw.isBlank()) return@forEach
+            val cols = parseLine(raw)
+            val key = cols.getOrNull(idxKey)?.trim().orEmpty()
+            val type = cols.getOrNull(idxType)?.trim()?.lowercase(Locale.ROOT).orEmpty()
+            if (key.isEmpty()) return@forEach
+            when (type) {
+                "text" -> {
+                    val value = cols.getOrNull(idxVal).orEmpty()
+                    map[key] = I18nEntry(key, I18nType.TEXT, value = value)
+                }
+                "plural" -> {
+                    val value = cols.getOrNull(idxVal).orEmpty()
+                    val parts = splitPairs(value)
+                    map[key] = I18nEntry(key, I18nType.PLURAL, plurals = parts)
+                }
+                "select" -> {
+                    val value = cols.getOrNull(idxVal).orEmpty()
+                    val parts = splitPairs(value, lowerKey = true)
+                    map[key] = I18nEntry(key, I18nType.SELECT, selects = parts)
+                }
+            }
+        }
+        return map
+    }
+
+    /** 合表 CSV:key,type,comment,<localeTag...> */
+    fun parseMerged(br: BufferedReader, locale: Locale): Map<String, I18nEntry> {
+        val header = parseLine(br.readLine() ?: return emptyMap())
+        val idxKey = header.indexOf("key")
+        val idxType = header.indexOf("type")
+        val idxVal = header.indexOf(locale.toLanguageTag())
+        if (idxKey < 0 || idxType < 0 || idxVal < 0) return emptyMap()
+
+        val map = LinkedHashMap<String, I18nEntry>(1024)
+        br.lineSequence().forEach { raw ->
+            if (raw.isBlank()) return@forEach
+            val cols = parseLine(raw)
+            val key = cols.getOrNull(idxKey)?.trim().orEmpty()
+            val type = cols.getOrNull(idxType)?.trim()?.lowercase(Locale.ROOT).orEmpty()
+            val valCol = cols.getOrNull(idxVal).orEmpty()
+            if (key.isEmpty()) return@forEach
+            when (type) {
+                "text" -> map[key] = I18nEntry(key, I18nType.TEXT, value = valCol)
+                "plural" -> map[key] = I18nEntry(key, I18nType.PLURAL, plurals = splitPairs(valCol))
+                "select" -> map[key] = I18nEntry(key, I18nType.SELECT, selects = splitPairs(valCol, true))
+            }
+        }
+        return map
+    }
+
+    /** 将 "one=xx|other=yy" 解析为 Map */
+    private fun splitPairs(raw: String, lowerKey: Boolean = false): Map<String, String> {
+        if (raw.isEmpty()) return emptyMap()
+        val out = LinkedHashMap<String, String>(4)
+        val seg = raw.split('|')
+        for (s in seg) {
+            val p = s.split('=', limit = 2)
+            if (p.size == 2) {
+                val k = if (lowerKey) p[0].trim().lowercase(Locale.ROOT) else p[0].trim()
+                out[k] = p[1]
+            }
+        }
+        return out
+    }
+}

+ 1 - 0
ui-base/src/main/java/com/grkj/ui_base/business/BleBusinessManager.kt

@@ -603,6 +603,7 @@ object BleBusinessManager {
             }
             if (isNeedLoading) LoadingEvent.sendLoadingEvent("工作票完成状态读取完成", true)
             logger.info("Get ticket status complete : ${bleDevice.mac}")
+            BleReturnDispatcher.removeBusy(bleDevice.mac)
             // TD:Ticket Done
             if (isNeedLoading) LoadingEvent.sendLoadingEvent("TD$ticketJson}", true)
 

+ 19 - 2
ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleConnectionManager.kt

@@ -466,8 +466,25 @@ object BleConnectionManager {
             if (isTimeout) {
                 logger.error("getCurrentStatus timeout : mac = ${bleDevice.mac}, retryCount = $retryCount")
                 if (retryCount > 0) {
-                    ThreadUtils.runOnMainDelayed(1000) {
-                        getCurrentStatus(from, bleDevice, retryCount - 1, timeoutCallBack)
+                    if (from != 4) {
+                        val canConnect = BleSendDispatcher.canConnect()
+                        logger.info("发送队列是否可以连接:${canConnect}")
+                        if (BleSendDispatcher.isConnected(bleDevice.mac)) {
+                            BleSendDispatcher.scheduleDisconnect(bleDevice.mac)
+                        }
+                        BleSendDispatcher.submit(bleDevice.mac) {
+                            getCurrentStatus(
+                                from, bleDevice, retryCount - 1, timeoutCallBack
+                            )
+                        }
+                    } else {
+                        BleReturnDispatcher.clearNoBusyConnectedDevice()
+                        BleReturnDispatcher.scheduleDisconnect(bleDevice.mac)
+                        BleReturnDispatcher.submit(bleDevice.mac) {
+                            getCurrentStatus(
+                                from, bleDevice, retryCount - 1, timeoutCallBack
+                            )
+                        }
                     }
                 } else {
                     BleSendDispatcher.scheduleDisconnect(bleDevice.mac)

+ 28 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleQueueDispatcher.kt

@@ -32,6 +32,11 @@ abstract class BleQueueDispatcher {
      */
     private val activeMacs = mutableMapOf<String, MutableList<(Boolean) -> Unit>>()
 
+    /**
+     * 繁忙设备
+     */
+    private val busyMacs = mutableSetOf<String>()
+
     /**
      * 已连接的设备
      */
@@ -197,4 +202,27 @@ abstract class BleQueueDispatcher {
      */
     fun shouldDisconnect(mac: String): Boolean =
         isConnecting(mac) || isQueued(mac) || isConnected(mac)
+
+    /**
+     * mac加入工作状态
+     */
+    fun busy(mac: String) {
+        busyMacs.add(mac)
+    }
+
+    /**
+     * mac移除工作状态
+     */
+    fun removeBusy(mac: String) {
+        busyMacs.remove(mac)
+    }
+
+    /**
+     * 清除不繁忙的连接设备
+     */
+    fun clearNoBusyConnectedDevice() {
+        connectedMacs.filter { it in busyMacs }.forEach {
+            scheduleDisconnect(it)
+        }
+    }
 }

+ 0 - 1
ui-base/src/main/res/values-en/strings.xml

@@ -28,7 +28,6 @@
     <string name="settings">Settings</string>
     <string name="back">Back</string>
     <string name="finish_the_job">Finish</string>
-    <string name="cancel_the_job">Cancel</string>
     <string name="action_confirm">Action Confirmation</string>
     <string name="recognize_work_content">Identify Work Content</string>
     <string name="power_isolation_way">Energy Isolation Method</string>

+ 0 - 6
ui-base/src/main/res/values-zh/strings.xml

@@ -24,7 +24,6 @@
     <string name="settings">设置</string>
     <string name="back">返回</string>
     <string name="finish_the_job">结束作业</string>
-    <string name="cancel_the_job">取消作业</string>
     <string name="action_confirm">操作确认</string>
     <string name="recognize_work_content">识别工作内容</string>
     <string name="power_isolation_way">确认隔离方式</string>
@@ -63,11 +62,6 @@
     <string name="fingerprint_delete_confirm_tip">确定要删除%s吗?</string>
     <string name="fingerprint_scan_tip">请连续按压%d次指纹识别区</string>
     <string name="fingerprint_add_success_tip">已成功添加指纹数据</string>
-    <string name="exception_type_tip">请选择异常类型</string>
-    <string name="exception_level_tip">请选择异常等级</string>
-    <string name="exception_submit_success_tip">异常提交成功</string>
-    <string name="add_face">录入人脸</string>
-    <string name="face_config_tip">最多可以录入1组人脸数据</string>
     <string name="recapture">重拍</string>
     <string name="capture_tip_title">录入提示</string>
     <string name="capture_tip_content">1. 系统将自动拍摄照片,在拍摄过程中请确保:

+ 4 - 3
ui-base/src/main/res/values/ids.xml

@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <item type="id" name="expand_tag_parent"/>
-    <item type="id" name="expand_tag_lp"/>
-    <item type="id" name="expand_tag_index"/>
+    <item name="expand_tag_parent" type="id" />
+    <item name="expand_tag_lp" type="id" />
+    <item name="expand_tag_index" type="id" />
+    <item name="i18n_locale_observer_job" type="id" />
 </resources>

+ 0 - 1
ui-base/src/main/res/values/strings.xml

@@ -28,7 +28,6 @@
     <string name="settings">设置</string>
     <string name="back">返回</string>
     <string name="finish_the_job">结束作业</string>
-    <string name="cancel_the_job">取消作业</string>
     <string name="action_confirm">操作确认</string>
     <string name="recognize_work_content">识别工作内容</string>
     <string name="power_isolation_way">确认隔离方式</string>