FlutterUnit 周边 | 组件数据国际化完成

上一篇介绍了通过 Deepseek 完成 FlutterUnit 组件数据的 10 国语言翻译,本篇将解析这些数据,并且重新设计数据库,让 FlutterUnit 在应用层的数据支持多国语言。

英文列表 英文详情
中文列表 中文详情

1. 数据库的改造

之前的数据库没有考虑到需要进行数据的国际化,所以介绍信息也是直接作为表中的字段,拿 widget 表来说,承载了所有界面表现需要的字段,

  • widget 表


现在要将国际化的数据通过表记录,考虑到后续的可拓展性,比如还有添加其他的语言,或者增加其他需要国际化的字段。这里打算保证 widget 表数据的唯一性,增加 widget_desc 表记录组件国际化相关的数据,通过 widget_id 和 widget 表进行关联。如下是 Container 组件在数据库中的存储形式:

  • widget 表,负责确保组件的唯一性:

  • widget_desc 表,负责记录组件介绍的国际化描述文本:


node 表记录组件介绍节点,一个组件对应多个 node 数据,节点的介绍信息也是同理,拆分出由 node_desc 表维护组件节点的介绍信息:

  • node 表

  • node_desc 表


2. 收集数据与解析

数据表定义完毕,接下来的目标收集上一篇章,Deepseek AI 接口生成的翻译文件。将他们整合在一起,录入数据库:

对于数据的收集,这里定义了 3 个对象:

  • WidgetData 负责收录一个组件对应的全部信息。
  • WidgetDesc 记录组件国际化的描述信息。
  • NodeData 记录一个组件介绍节点持有的数据信息。

其中一个 WidgetData 持有若干个国际化描述,以及国际化和组件节点的映射数据:

class WidgetData {
  final int id;
  final String name;
  final List<WidgetDesc> desc;
  final int family;
  final double lever;
  final int deprecated;
  final String linkWidget;
  final Map<String, List<NodeData>> nodes;
  ...
}

class WidgetDesc {
  final String locale;
  final String name;
  final String info;
  ...
}

class NodeData {
  final int id;
  final int widgetId;
  final String name;
  final String desc;
  final String locale;
  final int priority;
  final String filepath;
  ...
}

定义 WidgetParser 解析类维护同步解析逻辑,在构造时传入组件的总目录:

class WidgetParser {
  final String widgetPackage;

  WidgetParser(this.widgetPackage);

  Future<WidgetData> parserWidget(String widgetDir) async {
    // TODO 解析组件文件夹,收录信息
  }
  
}

和上篇处理组件翻译类似,这里通过 parserWidget 解析指定路径的组件。其中的过程包括:

  • _findDescFiles : 寻找组件文件夹下的国际化描述数据文件。
    • _parserNodes : 解析描述中的节点数据,构造节点列表和语言的映射。
  • 解析所有对的描述文件,构造一个 WidgetData 对象。
Iterable<String> _findDescFiles(String dir){
  Directory directory = Directory(dir);
  List<FileSystemEntity> files = directory.listSync();
  return files
      .where((e) => (e is File) && p.basename(e.path).startsWith('desc_'))
      .map((e) => e.path);
}

Future<WidgetData> parserWidget(String widgetDir) async {
  Iterable<String> descPathList = _findDescFiles(widgetDir);
  int id = 0;
  String name = '';
  List<WidgetDesc> desc = [];
  int family = 0;
  double lever = 0;
  int deprecated = 0;
  String linkWidget = '';
  Map<String, List<NodeData>> nodesMap = {};
  
  for (String descPath in descPathList) {
    String filename = p.basenameWithoutExtension(descPath);
    String locale = filename.replaceAll('desc_', '');
    locale = locale.replaceAll('_', '-').toLowerCase();
    dynamic descMap = json.decode(await File(descPath).readAsString());
    descMap['locale'] = locale;
    if (filename.contains('zh-CN')) {
      id = descMap['id'] ?? 0;
      name = descMap['name'] ?? '';
      family = descMap['family'] ?? 0;
      lever = descMap['lever'].toDouble() ?? 0;
      deprecated = descMap['deprecated'] ?? 0;
      linkWidget = descMap["linkIds"].join(',') ?? '';
    }
    desc.add(WidgetDesc.fromMap(descMap));
    nodesMap[locale] = _parserNodes(widgetDir, descMap);
  }
  return WidgetData(
    id: id,
    name: name,
    desc: desc,
    family: family,
    lever: lever,
    linkWidget: linkWidget,
    deprecated: deprecated,
    nodes: nodesMap,
  );
}

解析完毕后,得到的 WidgetData 对象,就持有了实现数据库国际化的所有数据。接下来的工作是如何把这个对象录入到数据库中。

List<NodeData> _parserNodes(String directory, dynamic descMap) {
  List<NodeData> nodes = [];
  int priority = 0;
  for (dynamic node in descMap['nodes']) {
    nodes.add(NodeData(
      id: -1,
      widgetId: descMap['id'],
      locale: descMap['locale'],
      name: node['name'],
      filepath: p.join(directory, node['file']),
      desc: node['desc'].join('\n'),
      priority: priority,
    ));
    priority++;
  }
  return nodes;
}

3. 数据库录入

这里定义 WidgetSync 类来同步数据库内容,其中:

  • 依赖 WidgetParser 解析得到 WidgetData 对象
  • WidgetDao 负责数据库操作,insertWidget 方法负责将 WidgetData 中的数据录入数据库。
class WidgetSync {
  final String project;

  WidgetSync(this.project);

  late WidgetParser parser = WidgetParser(project);
  
  WidgetDao dao = WidgetDao();
  
  Future<void> saveWidget(String widgetDir) async {
    WidgetData data = await parser.parserWidget(widgetDir);
    await dao.insertWidget(data);
  }
}

数据的录入主要就是通过 sql 语句,将对象记录的数据插入到 widgetwidget_descnodenode_desc 四张表里。以供 app 实现组件信息的展示功能。

Future<void> insertWidget(WidgetData data) async {
    final db = await FlutterDb.db.database;
    String insertWidget = """
INSERT INTO 
  widget 
  (id,name,family,lever,deprecated,linkWidget) 
VALUES 
  (?,?,?,?,?,?);
    """;
    List<Object?> args = [data.id, data.name, data.family, data.lever, data.deprecated, data.linkWidget];
    await db.rawInsert(insertWidget, args);

    String insertWidgetDesc = """
INSERT INTO 
  widget_desc 
  (widget_id,name,info,locale) 
VALUES 
  (?,?,?,?);
    """;

    for (WidgetDesc desc in data.desc) {
      List<Object?> args = [data.id, desc.name, desc.info, desc.locale];
      await db.rawInsert(insertWidgetDesc, args);
    }
    await insertNodes(data.nodes);
  }

WidgetData 是一个组件的全量数据,包含介绍的节点列表,所以插入过程中可以同时处理 node 表数据的插入:

  Future<int> insertNode(NodeData node) async {
    int id = flake.id();
    final db = await FlutterDb.db.database;
    String insertWidget = """
INSERT INTO node 
(id,widgetId,priority,code) 
VALUES (?,?,?,?);
    """;
    String code = await File(node.filepath).readAsString();
    List<Object?> args = [id, node.widgetId, node.priority, code];
    await db.rawInsert(insertWidget, args);
    return id;
  }

  Future<void> insertNodeDesc(int id, NodeData node) async {
    final db = await FlutterDb.db.database;
    String insertWidget = """
INSERT INTO node_desc 
(node_id,name,subtitle,locale) 
VALUES (?,?,?,?);
    """;
    List<Object?> args = [id, node.name, node.desc, node.locale];
    await db.rawInsert(insertWidget, args);
  }

此时调用 WidgetSync#saveWidget 就可以完成一个组件的所有数据录入。最后,只需要遍历所有的组件,触发 saveWidget 即可。这样就得到了对 FlutterUnit 组件展示来说至关重要的数据库: flutter.db

final Map<int, String> familyMap = {
  0: 'StatelessWidget',
  1: 'StatefulWidget',
  2: 'SingleChildRenderObjectWidget',
  3: 'MultiChildRenderObjectWidget',
  4: 'Sliver',
  5: 'ProxyWidget',
  6: 'Other',
};

Future<void> syncAll() async {
  for (String family in familyMap.values) {
    await syncFamily(family);
  }
}

Future<void> syncFamily(String family) async {
  String dir = p.join(project, 'modules', 'widget_system', 'widgets', 'lib');
  String familyDir = p.join(dir, family);
  Directory directory = Directory(familyDir);
  List<FileSystemEntity> entity = directory.listSync();
  for (FileSystemEntity e in entity) {
    if (e is Directory) {
      await saveWidget(e.path);
    }
  }
}

5. FlutterUnit 应用层的处理

数据库目前已经内含 10 国语言的数据,接下来需要升级应用层的查询处理。之前介绍过,FlutterUnit 采用模块化的设计:

  • widget: 负责维护所有的组件对应的案例代码
  • widget_module: 负责组件展示的业务逻辑和界面构建逻辑。
  • widget_repository:负责组件相关数据的操作逻辑。

当前增加数据库多语言查询,其实对业务逻辑和视图构建没有太大的影响。只需要处理 widget_repository 中数据库查询操作即可:比如 WidgetDao 中, queryWidgetByName 方法根据组件名查询组件,现在增加 locale 参数表示查询的语言,通过 widget 和 widget_desc 联表查询,就可以得到之前的数据结构。上层的模型层、视图层都不用任何变化:

  Future<Map<String, dynamic>?> queryWidgetByName(String name, {String? locale}) async {
    String querySql = """
SELECT 
  widget.id, 
  widget.name,
  widget.family,
  widget.linkWidget,
  widget.lever,
  widget_desc.name AS nameCN, 
  widget_desc.info
FROM widget
INNER JOIN widget_desc 
  ON widget.id = widget_desc.widget_id
WHERE 
widget.name = ? AND 
widget_desc.locale = ? 
;
""";
    List<Map<String, Object?>> result = await database.rawQuery(querySql, [name,locale??'zh-cn']);
    if (result.isNotEmpty) {
      return result.first;
    }
    return null;
  }

这就是模块化以及合理划分层次的好处,新的需求来临时,只需要修改和它相关模块的相关层级即可。外界只在乎它提供的能力,不在意其具体的功能实现,这样就可以局部地升级代码,不会对整个系统产生其他影响。最后在查询时,业务层逻辑的事件,只需要额外承载 locale 参数,作为查询的参数,就可以将 旧的螺丝钉 换成 支持十国语言的新螺丝钉

extension WidgetContext on BuildContext{

  void switchWidgetFamily(WidgetFamily family){
    Locale locale = read<AppConfigBloc>().state.language.locale;
    String lang = '${locale.languageCode}-${locale.countryCode}'.toLowerCase();
    read<WidgetsBloc>().add(EventTabTap(family,locale: lang));
  }
}

本文简单介绍了一下 FlutterUnit 支持十国语言的过程,具体细节可以自己查阅开源的项目源码。在 AI 大模型的能力加持下,普通的开发者可以调度更加强大的力量,完成在之前很难实现的功能。包括目前正在进行的 Flutter 组件 Logo 设计,也有 AI 帮忙设计和优化。大家敬请期待 FlutterUnit 的逐步完善,感谢支持 ~

注册登录 后评论
    // 作者
    张风捷特烈 发布于 掘金
    • 0
    // 本帖子
    分类
    // 相关帖子
    Coming soon...
    • 0
    FlutterUnit 周边 | 组件数据国际化完成张风捷特烈 发布于 掘金