鸿蒙
This commit is contained in:
@@ -1,368 +0,0 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 鸿蒙端权限一致性校验脚本
|
||||
/// 创建时间: 2026-05-31
|
||||
/// 更新时间: 2026-05-31
|
||||
/// 作用: 校验 module.json5 与 string.json 之间的权限声明一致性
|
||||
/// - 检查 module.json5 中权限的 reason 引用是否在 string.json 中存在
|
||||
/// - 检查 string.json 中 permission_*_reason 键是否被 module.json5 引用
|
||||
/// - 检查 user_grant 权限是否缺少 reason 说明
|
||||
/// 上次更新: 初始创建
|
||||
/// ============================================================
|
||||
///
|
||||
/// 用法:
|
||||
/// dart run tools/check_ohos_permissions.dart
|
||||
///
|
||||
/// 退出码:
|
||||
/// 0 — 校验通过,无错误
|
||||
/// 1 — 存在错误(缺失引用或未使用的键)
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
// ── 路径常量 ──────────────────────────────────────────────────
|
||||
|
||||
const String projectRoot = 'e:\\project\\flutter\\f\\xianyan';
|
||||
const String moduleJson5Path =
|
||||
'$projectRoot\\ohos\\entry\\src\\main\\module.json5';
|
||||
const String stringJsonPath =
|
||||
'$projectRoot\\ohos\\entry\\src\\main\\resources\\base\\element\\string.json';
|
||||
|
||||
// ── 终端颜色 ──────────────────────────────────────────────────
|
||||
|
||||
const String _reset = '\x1B[0m';
|
||||
const String _red = '\x1B[31m';
|
||||
const String _green = '\x1B[32m';
|
||||
const String _yellow = '\x1B[33m';
|
||||
const String _cyan = '\x1B[36m';
|
||||
const String _bold = '\x1B[1m';
|
||||
|
||||
// ── 数据模型 ──────────────────────────────────────────────────
|
||||
|
||||
/// module.json5 中的权限条目
|
||||
class PermissionEntry {
|
||||
final String name;
|
||||
final String? reasonRef;
|
||||
final String? when_;
|
||||
|
||||
PermissionEntry({
|
||||
required this.name,
|
||||
this.reasonRef,
|
||||
this.when_,
|
||||
});
|
||||
}
|
||||
|
||||
// ── JSON5 解析 ────────────────────────────────────────────────
|
||||
|
||||
/// 将 JSON5 文本转换为标准 JSON
|
||||
/// 处理: 单行注释、多行注释、尾逗号
|
||||
String _json5ToJson(String source) {
|
||||
final buffer = StringBuffer();
|
||||
var i = 0;
|
||||
|
||||
while (i < source.length) {
|
||||
// 单行注释 //
|
||||
if (i < source.length - 1 && source[i] == '/' && source[i + 1] == '/') {
|
||||
while (i < source.length && source[i] != '\n') {
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 多行注释 /* */
|
||||
if (i < source.length - 1 && source[i] == '/' && source[i + 1] == '*') {
|
||||
i += 2;
|
||||
while (i < source.length - 1) {
|
||||
if (source[i] == '*' && source[i + 1] == '/') {
|
||||
i += 2;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 字符串字面量 — 原样保留,避免误判注释
|
||||
if (source[i] == '"' || source[i] == "'") {
|
||||
final quote = source[i];
|
||||
buffer.write('"');
|
||||
i++;
|
||||
while (i < source.length && source[i] != quote) {
|
||||
if (source[i] == '\\' && i + 1 < source.length) {
|
||||
buffer.write(source[i]);
|
||||
buffer.write(source[i + 1]);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (source[i] == '"') {
|
||||
buffer.write('\\"');
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
buffer.write(source[i]);
|
||||
i++;
|
||||
}
|
||||
buffer.write('"');
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer.write(source[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
var result = buffer.toString();
|
||||
|
||||
// 移除尾逗号: , 后跟 } 或 ]
|
||||
result = result.replaceAll(RegExp(r',\s*([}\]])'), r'$1');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 解析 module.json5 文件,提取 requestPermissions 列表
|
||||
List<PermissionEntry> _parseModuleJson5(String filePath) {
|
||||
final file = File(filePath);
|
||||
if (!file.existsSync()) {
|
||||
stderr.writeln('${_red}错误: 文件不存在 $filePath$_reset');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
final raw = file.readAsStringSync();
|
||||
final jsonStr = _json5ToJson(raw);
|
||||
|
||||
late final Map<String, dynamic> root;
|
||||
try {
|
||||
root = json.decode(jsonStr) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
stderr.writeln('${_red}错误: JSON5 解析失败 — $e$_reset');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
final module = root['module'] as Map<String, dynamic>?;
|
||||
if (module == null) {
|
||||
stderr.writeln('${_red}错误: module.json5 缺少 module 根节点$_reset');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
final permissions = module['requestPermissions'] as List<dynamic>?;
|
||||
if (permissions == null) {
|
||||
stderr.writeln('${_yellow}警告: module.json5 无 requestPermissions 声明$_reset');
|
||||
return [];
|
||||
}
|
||||
|
||||
return permissions.map((p) {
|
||||
final map = p as Map<String, dynamic>;
|
||||
String? reasonRef;
|
||||
final reason = map['reason'];
|
||||
if (reason is String && reason.startsWith('\$string:')) {
|
||||
reasonRef = reason.substring('\$string:'.length);
|
||||
}
|
||||
|
||||
String? when_;
|
||||
final usedScene = map['usedScene'] as Map<String, dynamic>?;
|
||||
if (usedScene != null) {
|
||||
when_ = usedScene['when'] as String?;
|
||||
}
|
||||
|
||||
return PermissionEntry(
|
||||
name: map['name'] as String,
|
||||
reasonRef: reasonRef,
|
||||
when_: when_,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// 解析 string.json 文件,提取所有 permission_*_reason 键
|
||||
Map<String, String> _parseStringJson(String filePath) {
|
||||
final file = File(filePath);
|
||||
if (!file.existsSync()) {
|
||||
stderr.writeln('${_red}错误: 文件不存在 $filePath$_reset');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
final raw = file.readAsStringSync();
|
||||
late final Map<String, dynamic> root;
|
||||
try {
|
||||
root = json.decode(raw) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
stderr.writeln('${_red}错误: string.json 解析失败 — $e$_reset');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
final strings = root['string'] as List<dynamic>;
|
||||
final result = <String, String>{};
|
||||
|
||||
for (final item in strings) {
|
||||
final map = item as Map<String, dynamic>;
|
||||
final name = map['name'] as String;
|
||||
if (name.startsWith('permission_') && name.endsWith('_reason')) {
|
||||
result[name] = map['value'] as String;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── 已知的 user_grant 权限列表 ────────────────────────────────
|
||||
|
||||
/// 鸿蒙系统需要用户授权的权限
|
||||
/// 参考: https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/permissions
|
||||
const Set<String> _userGrantPermissions = {
|
||||
'ohos.permission.CAMERA',
|
||||
'ohos.permission.MICROPHONE',
|
||||
'ohos.permission.LOCATION',
|
||||
'ohos.permission.APPROXIMATELY_LOCATION',
|
||||
'ohos.permission.READ_MEDIA',
|
||||
'ohos.permission.WRITE_MEDIA',
|
||||
'ohos.permission.ACCESS_BLUETOOTH',
|
||||
'ohos.permission.NFC_TAG',
|
||||
};
|
||||
|
||||
// ── 校验逻辑 ──────────────────────────────────────────────────
|
||||
|
||||
/// 校验结果
|
||||
class CheckResult {
|
||||
final List<String> matched = [];
|
||||
final List<String> missingReasons = [];
|
||||
final List<String> unusedReasons = [];
|
||||
final List<String> permissionsWithoutReason = [];
|
||||
|
||||
bool get hasErrors => missingReasons.isNotEmpty || unusedReasons.isNotEmpty;
|
||||
bool get hasWarnings => permissionsWithoutReason.isNotEmpty;
|
||||
}
|
||||
|
||||
CheckResult _validate(
|
||||
List<PermissionEntry> permissions,
|
||||
Map<String, String> reasonStrings,
|
||||
) {
|
||||
final result = CheckResult();
|
||||
|
||||
// 收集 module.json5 中引用的所有 reason key
|
||||
final referencedReasons = <String>{};
|
||||
|
||||
for (final perm in permissions) {
|
||||
if (perm.reasonRef != null) {
|
||||
referencedReasons.add(perm.reasonRef!);
|
||||
|
||||
if (reasonStrings.containsKey(perm.reasonRef)) {
|
||||
result.matched.add(perm.name);
|
||||
} else {
|
||||
result.missingReasons.add(
|
||||
'${perm.name} → \$string:${perm.reasonRef}',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// user_grant 权限应该有 reason
|
||||
if (_userGrantPermissions.contains(perm.name)) {
|
||||
result.permissionsWithoutReason.add(perm.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 string.json 中未被引用的 reason key
|
||||
for (final key in reasonStrings.keys) {
|
||||
if (!referencedReasons.contains(key)) {
|
||||
result.unusedReasons.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── 报告输出 ──────────────────────────────────────────────────
|
||||
|
||||
void _printReport(CheckResult result) {
|
||||
stdout.writeln();
|
||||
stdout.writeln('${_bold}╔══════════════════════════════════════════════════╗$_reset');
|
||||
stdout.writeln('${_bold}║ 鸿蒙权限一致性校验报告 ║$_reset');
|
||||
stdout.writeln('${_bold}╚══════════════════════════════════════════════════╝$_reset');
|
||||
stdout.writeln();
|
||||
|
||||
// ✅ 匹配项
|
||||
stdout.writeln(
|
||||
'${_green}✅ 匹配的权限 (${result.matched.length})$_reset',
|
||||
);
|
||||
if (result.matched.isEmpty) {
|
||||
stdout.writeln(' (无)');
|
||||
} else {
|
||||
for (final name in result.matched) {
|
||||
stdout.writeln(' $name');
|
||||
}
|
||||
}
|
||||
stdout.writeln();
|
||||
|
||||
// ❌ 缺失的 reason 字符串
|
||||
stdout.writeln(
|
||||
'${_red}❌ 缺失的 reason 字符串 (${result.missingReasons.length})$_reset',
|
||||
);
|
||||
if (result.missingReasons.isEmpty) {
|
||||
stdout.writeln(' (无)');
|
||||
} else {
|
||||
for (final item in result.missingReasons) {
|
||||
stdout.writeln(' $item');
|
||||
}
|
||||
}
|
||||
stdout.writeln();
|
||||
|
||||
// ⚠️ 未使用的 reason 字符串
|
||||
stdout.writeln(
|
||||
'${_yellow}⚠️ 未使用的 reason 字符串 (${result.unusedReasons.length})$_reset',
|
||||
);
|
||||
if (result.unusedReasons.isEmpty) {
|
||||
stdout.writeln(' (无)');
|
||||
} else {
|
||||
for (final key in result.unusedReasons) {
|
||||
stdout.writeln(' $key');
|
||||
}
|
||||
}
|
||||
stdout.writeln();
|
||||
|
||||
// ⚠️ 缺少 reason 的 user_grant 权限
|
||||
stdout.writeln(
|
||||
'${_yellow}⚠️ 缺少 reason 的 user_grant 权限 (${result.permissionsWithoutReason.length})$_reset',
|
||||
);
|
||||
if (result.permissionsWithoutReason.isEmpty) {
|
||||
stdout.writeln(' (无)');
|
||||
} else {
|
||||
for (final name in result.permissionsWithoutReason) {
|
||||
stdout.writeln(' $name');
|
||||
}
|
||||
}
|
||||
stdout.writeln();
|
||||
|
||||
// 总结
|
||||
stdout.writeln('${_cyan}──────────────────────────────────────────────────$_reset');
|
||||
if (result.hasErrors) {
|
||||
stdout.writeln(
|
||||
'${_red}${_bold}结果: 失败 — 发现 ${result.missingReasons.length} 个缺失引用, ${result.unusedReasons.length} 个未使用键$_reset',
|
||||
);
|
||||
} else if (result.hasWarnings) {
|
||||
stdout.writeln(
|
||||
'${_yellow}${_bold}结果: 通过(有警告) — ${result.permissionsWithoutReason.length} 个 user_grant 权限缺少 reason$_reset',
|
||||
);
|
||||
} else {
|
||||
stdout.writeln(
|
||||
'${_green}${_bold}结果: 通过 — 所有权限声明一致$_reset',
|
||||
);
|
||||
}
|
||||
stdout.writeln('${_cyan}──────────────────────────────────────────────────$_reset');
|
||||
stdout.writeln();
|
||||
}
|
||||
|
||||
// ── 入口 ──────────────────────────────────────────────────────
|
||||
|
||||
void main() {
|
||||
stdout.writeln('${_cyan}正在解析 module.json5...$_reset');
|
||||
final permissions = _parseModuleJson5(moduleJson5Path);
|
||||
stdout.writeln(' 找到 ${permissions.length} 个权限声明');
|
||||
|
||||
stdout.writeln('${_cyan}正在解析 string.json...$_reset');
|
||||
final reasonStrings = _parseStringJson(stringJsonPath);
|
||||
stdout.writeln(' 找到 ${reasonStrings.length} 个 permission_*_reason 键');
|
||||
|
||||
final result = _validate(permissions, reasonStrings);
|
||||
_printReport(result);
|
||||
|
||||
if (result.hasErrors) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,730 +0,0 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 翻译代码生成与检查脚本
|
||||
/// 创建时间: 2026-05-31
|
||||
/// 更新时间: 2026-05-31
|
||||
/// 作用: 读取zh_cn.dart基准语言,解析所有翻译类型,
|
||||
/// 检查语言文件字段完整性,输出缺失字段报告
|
||||
/// 上次更新: 初始创建
|
||||
/// ============================================================
|
||||
///
|
||||
/// 用法:
|
||||
/// dart run tools/gen_l10n.dart --check 检查所有语言文件的字段完整性
|
||||
/// dart run tools/gen_l10n.dart --diff 输出各语言与zh_cn的差异
|
||||
/// dart run tools/gen_l10n.dart --skeleton 生成类型定义文件骨架
|
||||
/// dart run tools/gen_l10n.dart --report 输出完整覆盖率报告
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
// ── 常量 ──────────────────────────────────────────────────────
|
||||
|
||||
const String projectRoot = 'e:\\project\\flutter\\f\\xianyan';
|
||||
const String l10nDir = '$projectRoot\\lib\\l10n';
|
||||
const String typesDir = '$l10nDir\\types';
|
||||
const String languagesDir = '$l10nDir\\languages';
|
||||
const String baseLanguageFile = '$languagesDir\\zh_cn.dart';
|
||||
|
||||
/// 所有语言文件映射
|
||||
const Map<String, String> languageFiles = {
|
||||
'zh_CN': 'zh_cn.dart',
|
||||
'en': 'en.dart',
|
||||
'ja': 'ja.dart',
|
||||
'zh_TW': 'zh_tw.dart',
|
||||
'ko': 'ko.dart',
|
||||
'de': 'de.dart',
|
||||
'it': 'it.dart',
|
||||
'es': 'es.dart',
|
||||
'ar': 'ar.dart',
|
||||
'bn': 'bn.dart',
|
||||
'hi': 'hi.dart',
|
||||
'pt': 'pt.dart',
|
||||
'ru': 'ru.dart',
|
||||
'fr': 'fr.dart',
|
||||
};
|
||||
|
||||
/// 所有翻译类型文件
|
||||
const List<String> typeFiles = [
|
||||
't_nav.dart',
|
||||
't_common.dart',
|
||||
't_home.dart',
|
||||
't_home_base.dart',
|
||||
't_sentence_detail.dart',
|
||||
't_read_later.dart',
|
||||
't_discover.dart',
|
||||
't_profile.dart',
|
||||
't_settings.dart',
|
||||
't_settings_interaction.dart',
|
||||
't_settings_display.dart',
|
||||
't_settings_performance.dart',
|
||||
't_settings_privacy.dart',
|
||||
't_settings_advanced.dart',
|
||||
't_settings_cache.dart',
|
||||
't_settings_permission.dart',
|
||||
't_settings_data_collection.dart',
|
||||
't_about.dart',
|
||||
't_onboarding.dart',
|
||||
't_progress.dart',
|
||||
't_root.dart',
|
||||
];
|
||||
|
||||
/// 组合类型(包含子模块而非直接String字段的类型)
|
||||
/// 这些类型在语言文件中以嵌套形式出现,需要特殊处理
|
||||
const Set<String> compositeTypes = {'THome', 'TSettings'};
|
||||
|
||||
// ── 解析器 ────────────────────────────────────────────────────
|
||||
|
||||
/// 解析类型文件中的字段定义
|
||||
/// 返回 {字段名: 注释} 的映射
|
||||
Map<String, String> parseTypeFields(String filePath) {
|
||||
final file = File(filePath);
|
||||
if (!file.existsSync()) {
|
||||
stderr.writeln(' ⚠️ 文件不存在: $filePath');
|
||||
return {};
|
||||
}
|
||||
|
||||
final content = file.readAsStringSync();
|
||||
final fields = <String, String>{};
|
||||
|
||||
// 匹配 final String fieldName; 模式
|
||||
final fieldRegex = RegExp(r'final\s+String\s+(\w+);');
|
||||
// 匹配注释行 /// xxx
|
||||
final commentRegex = RegExp(r'///\s*(.+)');
|
||||
|
||||
final lines = content.split('\n');
|
||||
String? lastComment;
|
||||
|
||||
for (final line in lines) {
|
||||
final trimmed = line.trim();
|
||||
|
||||
// 检查注释
|
||||
final commentMatch = commentRegex.firstMatch(trimmed);
|
||||
if (commentMatch != null) {
|
||||
lastComment = commentMatch.group(1)?.trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查字段声明
|
||||
final fieldMatch = fieldRegex.firstMatch(trimmed);
|
||||
if (fieldMatch != null) {
|
||||
final fieldName = fieldMatch.group(1)!;
|
||||
fields[fieldName] = lastComment ?? '';
|
||||
lastComment = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 非空行且非注释,重置注释
|
||||
if (trimmed.isNotEmpty && !trimmed.startsWith('///')) {
|
||||
lastComment = null;
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/// 解析类型文件中的toMap()字段映射
|
||||
Map<String, String> parseToMapFields(String filePath) {
|
||||
final file = File(filePath);
|
||||
if (!file.existsSync()) return {};
|
||||
|
||||
final content = file.readAsStringSync();
|
||||
final fields = <String, String>{};
|
||||
|
||||
// 匹配 toMap() 中的 'key': fieldName 模式
|
||||
final mapEntryRegex = RegExp(r"'(\w+)':\s*(\w+)");
|
||||
final inToMap = <bool>[false];
|
||||
|
||||
final lines = content.split('\n');
|
||||
for (final line in lines) {
|
||||
final trimmed = line.trim();
|
||||
if (trimmed.contains('toMap()')) {
|
||||
inToMap[0] = true;
|
||||
continue;
|
||||
}
|
||||
if (inToMap[0] && trimmed.contains('};')) {
|
||||
inToMap[0] = false;
|
||||
continue;
|
||||
}
|
||||
if (inToMap[0]) {
|
||||
final match = mapEntryRegex.firstMatch(trimmed);
|
||||
if (match != null) {
|
||||
fields[match.group(1)!] = match.group(2)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/// 解析语言文件中某个类型的字段值
|
||||
/// 支持嵌套类型(如 THome(base: THomeBase(...), sentenceDetail: TSentenceDetail(...)))
|
||||
Map<String, String> parseLanguageTypeFields(String content, String typeName) {
|
||||
final fields = <String, String>{};
|
||||
|
||||
final typeStartRegex = RegExp('$typeName\\s*\\(');
|
||||
final typeStartMatch = typeStartRegex.firstMatch(content);
|
||||
if (typeStartMatch == null) return {};
|
||||
|
||||
// 从 TypeName( 开始,找到匹配的 )
|
||||
var depth = 0;
|
||||
final start = typeStartMatch.end;
|
||||
final buffer = StringBuffer();
|
||||
|
||||
for (var i = start; i < content.length; i++) {
|
||||
final char = content[i];
|
||||
if (char == '(') {
|
||||
depth++;
|
||||
buffer.write(char);
|
||||
} else if (char == ')') {
|
||||
if (depth == 0) break;
|
||||
depth--;
|
||||
buffer.write(char);
|
||||
} else {
|
||||
buffer.write(char);
|
||||
}
|
||||
}
|
||||
|
||||
final typeContent = buffer.toString();
|
||||
|
||||
// 匹配 key: 'value' 或 key: "value" 模式(仅直接字段,不进入嵌套类型)
|
||||
final fieldRegex = RegExp(
|
||||
r"""(\w+):\s*(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)")""",
|
||||
);
|
||||
for (final match in fieldRegex.allMatches(typeContent)) {
|
||||
final key = match.group(1)!;
|
||||
final value = match.group(2) ?? match.group(3) ?? '';
|
||||
fields[key] = value;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/// 递归解析语言文件中某个类型的所有字段(包括嵌套子类型)
|
||||
/// 返回扁平化的字段映射,key格式为 "子类型.字段名"
|
||||
Map<String, String> parseLanguageTypeFieldsRecursive(
|
||||
String content,
|
||||
String typeName, {
|
||||
String prefix = '',
|
||||
}) {
|
||||
final fields = <String, String>{};
|
||||
|
||||
final typeStartRegex = RegExp('$typeName\\s*\\(');
|
||||
final typeStartMatch = typeStartRegex.firstMatch(content);
|
||||
if (typeStartMatch == null) return {};
|
||||
|
||||
// 从 TypeName( 开始,找到匹配的 )
|
||||
var depth = 0;
|
||||
final start = typeStartMatch.end;
|
||||
final buffer = StringBuffer();
|
||||
|
||||
for (var i = start; i < content.length; i++) {
|
||||
final char = content[i];
|
||||
if (char == '(') {
|
||||
depth++;
|
||||
buffer.write(char);
|
||||
} else if (char == ')') {
|
||||
if (depth == 0) break;
|
||||
depth--;
|
||||
buffer.write(char);
|
||||
} else {
|
||||
buffer.write(char);
|
||||
}
|
||||
}
|
||||
|
||||
final typeContent = buffer.toString();
|
||||
|
||||
// 匹配直接字符串字段
|
||||
final fieldRegex = RegExp(
|
||||
r"""(\w+):\s*(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)")""",
|
||||
);
|
||||
for (final match in fieldRegex.allMatches(typeContent)) {
|
||||
final key = match.group(1)!;
|
||||
final value = match.group(2) ?? match.group(3) ?? '';
|
||||
final fullKey = prefix.isNotEmpty ? '$prefix.$key' : key;
|
||||
fields[fullKey] = value;
|
||||
}
|
||||
|
||||
// 匹配嵌套类型字段 key: SubTypeName(
|
||||
final nestedRegex = RegExp(r'(\w+):\s*([A-Z]\w+)\s*\(');
|
||||
for (final match in nestedRegex.allMatches(typeContent)) {
|
||||
final subKey = match.group(1)!;
|
||||
final subTypeName = match.group(2)!;
|
||||
final fullPrefix = prefix.isNotEmpty ? '$prefix.$subKey' : subKey;
|
||||
|
||||
// 递归解析子类型
|
||||
// 需要从子类型的开始位置重新解析
|
||||
final subFields = _parseNestedType(content, subTypeName, match.start);
|
||||
for (final subEntry in subFields.entries) {
|
||||
final fullKey = '$fullPrefix.${subEntry.key}';
|
||||
fields[fullKey] = subEntry.value;
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/// 解析嵌套类型的字段
|
||||
Map<String, String> _parseNestedType(
|
||||
String content,
|
||||
String typeName,
|
||||
int searchFrom,
|
||||
) {
|
||||
final fields = <String, String>{};
|
||||
|
||||
// 从searchFrom位置开始查找TypeName(
|
||||
final subContent = content.substring(searchFrom);
|
||||
final typeStartRegex = RegExp('$typeName\\s*\\(');
|
||||
final typeStartMatch = typeStartRegex.firstMatch(subContent);
|
||||
if (typeStartMatch == null) return {};
|
||||
|
||||
var depth = 0;
|
||||
final start = typeStartMatch.end;
|
||||
final buffer = StringBuffer();
|
||||
|
||||
for (var i = start; i < subContent.length; i++) {
|
||||
final char = subContent[i];
|
||||
if (char == '(') {
|
||||
depth++;
|
||||
buffer.write(char);
|
||||
} else if (char == ')') {
|
||||
if (depth == 0) break;
|
||||
depth--;
|
||||
buffer.write(char);
|
||||
} else {
|
||||
buffer.write(char);
|
||||
}
|
||||
}
|
||||
|
||||
final typeContent = buffer.toString();
|
||||
|
||||
// 仅匹配直接字符串字段
|
||||
final fieldRegex = RegExp(
|
||||
r"""(\w+):\s*(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)")""",
|
||||
);
|
||||
for (final match in fieldRegex.allMatches(typeContent)) {
|
||||
final key = match.group(1)!;
|
||||
final value = match.group(2) ?? match.group(3) ?? '';
|
||||
fields[key] = value;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/// 获取类型名(从文件名)
|
||||
/// t_nav.dart -> TNav, t_home_base.dart -> THomeBase
|
||||
String getTypeName(String fileName) {
|
||||
final name = fileName.replaceAll('.dart', '');
|
||||
// 所有类型文件以 t_ 开头,去掉前缀 t_
|
||||
final withoutPrefix = name.startsWith('t_') ? name.substring(2) : name;
|
||||
final parts = withoutPrefix.split('_');
|
||||
final result = StringBuffer('T');
|
||||
for (final part in parts) {
|
||||
if (part.isNotEmpty) {
|
||||
result.write(part[0].toUpperCase());
|
||||
result.write(part.substring(1));
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
// ── 命令实现 ──────────────────────────────────────────────────
|
||||
|
||||
/// --check: 检查所有语言文件的字段完整性
|
||||
int checkCompleteness() {
|
||||
stdout.writeln('🔍 翻译字段完整性检查');
|
||||
stdout.writeln('=' * 60);
|
||||
|
||||
// 1. 解析基准语言 zh_cn.dart
|
||||
final baseFile = File(baseLanguageFile);
|
||||
if (!baseFile.existsSync()) {
|
||||
stderr.writeln('❌ 基准语言文件不存在: $baseLanguageFile');
|
||||
return 1;
|
||||
}
|
||||
final baseContent = baseFile.readAsStringSync();
|
||||
|
||||
// 2. 解析所有类型文件的字段定义
|
||||
stdout.writeln('\n📋 类型定义文件字段统计:');
|
||||
stdout.writeln('-' * 40);
|
||||
|
||||
final typeFieldsMap = <String, Map<String, String>>{};
|
||||
var totalFields = 0;
|
||||
|
||||
for (final typeFile in typeFiles) {
|
||||
if (typeFile == 't_root.dart') continue; // 根类型不包含翻译字段
|
||||
final filePath = '$typesDir\\$typeFile';
|
||||
final fields = parseTypeFields(filePath);
|
||||
if (fields.isNotEmpty) {
|
||||
final typeName = getTypeName(typeFile);
|
||||
typeFieldsMap[typeName] = fields;
|
||||
totalFields += fields.length;
|
||||
stdout.writeln(' $typeName: ${fields.length} 个字段');
|
||||
}
|
||||
}
|
||||
|
||||
stdout.writeln('\n 总计: $totalFields 个翻译字段');
|
||||
stdout.writeln('=' * 60);
|
||||
|
||||
// 3. 检查每种语言的字段完整性
|
||||
stdout.writeln('\n🌐 语言文件字段检查:');
|
||||
stdout.writeln('-' * 60);
|
||||
|
||||
var hasError = false;
|
||||
|
||||
for (final entry in languageFiles.entries) {
|
||||
final langId = entry.key;
|
||||
final fileName = entry.value;
|
||||
final filePath = '$languagesDir\\$fileName';
|
||||
|
||||
final file = File(filePath);
|
||||
if (!file.existsSync()) {
|
||||
stdout.writeln(' ❌ $langId: 文件不存在 ($fileName)');
|
||||
hasError = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
final content = file.readAsStringSync();
|
||||
var missingCount = 0;
|
||||
var emptyCount = 0;
|
||||
final missingFields = <String>[];
|
||||
final emptyFields = <String>[];
|
||||
|
||||
for (final typeEntry in typeFieldsMap.entries) {
|
||||
final typeName = typeEntry.key;
|
||||
final expectedFields = typeEntry.value;
|
||||
|
||||
// 组合类型使用递归解析
|
||||
final langFields = compositeTypes.contains(typeName)
|
||||
? parseLanguageTypeFieldsRecursive(content, typeName)
|
||||
: parseLanguageTypeFields(content, typeName);
|
||||
|
||||
for (final fieldEntry in expectedFields.entries) {
|
||||
final fieldName = fieldEntry.key;
|
||||
if (!langFields.containsKey(fieldName)) {
|
||||
missingFields.add('$typeName.$fieldName');
|
||||
missingCount++;
|
||||
} else if (langFields[fieldName]!.isEmpty) {
|
||||
emptyFields.add('$typeName.$fieldName');
|
||||
emptyCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final total = totalFields;
|
||||
final covered = total - missingCount - emptyCount;
|
||||
final percent = (covered * 100 / total).round();
|
||||
|
||||
final status = percent >= 90
|
||||
? '✅'
|
||||
: percent >= 70
|
||||
? '⚠️'
|
||||
: '❌';
|
||||
|
||||
stdout.writeln(
|
||||
' $status $langId ($fileName): $covered/$total ($percent%)'
|
||||
'${missingCount > 0 ? ' | 缺失: $missingCount' : ''}'
|
||||
'${emptyCount > 0 ? ' | 空值: $emptyCount' : ''}',
|
||||
);
|
||||
|
||||
if (missingFields.isNotEmpty) {
|
||||
stdout.writeln(
|
||||
' 缺失字段: ${missingFields.take(10).join(', ')}'
|
||||
'${missingFields.length > 10 ? ' ... (+${missingFields.length - 10})' : ''}',
|
||||
);
|
||||
}
|
||||
|
||||
if (emptyFields.isNotEmpty && emptyFields.length <= 5) {
|
||||
stdout.writeln(' 空值字段: ${emptyFields.join(', ')}');
|
||||
}
|
||||
}
|
||||
|
||||
stdout.writeln('=' * 60);
|
||||
return hasError ? 1 : 0;
|
||||
}
|
||||
|
||||
/// --diff: 输出各语言与zh_cn的差异
|
||||
int diffLanguages() {
|
||||
stdout.writeln('📊 翻译差异报告 (与 zh_CN 对比)');
|
||||
stdout.writeln('=' * 60);
|
||||
|
||||
final baseFile = File(baseLanguageFile);
|
||||
if (!baseFile.existsSync()) {
|
||||
stderr.writeln('❌ 基准语言文件不存在: $baseLanguageFile');
|
||||
return 1;
|
||||
}
|
||||
final baseContent = baseFile.readAsStringSync();
|
||||
|
||||
// 解析类型文件
|
||||
final typeFieldsMap = <String, Map<String, String>>{};
|
||||
for (final typeFile in typeFiles) {
|
||||
if (typeFile == 't_root.dart') continue;
|
||||
final filePath = '$typesDir\\$typeFile';
|
||||
final fields = parseToMapFields(filePath);
|
||||
if (fields.isNotEmpty) {
|
||||
final typeName = getTypeName(typeFile);
|
||||
typeFieldsMap[typeName] = fields;
|
||||
}
|
||||
}
|
||||
|
||||
// 解析基准语言
|
||||
final baseFields = <String, Map<String, String>>{};
|
||||
for (final typeEntry in typeFieldsMap.entries) {
|
||||
baseFields[typeEntry.key] = compositeTypes.contains(typeEntry.key)
|
||||
? parseLanguageTypeFieldsRecursive(baseContent, typeEntry.key)
|
||||
: parseLanguageTypeFields(baseContent, typeEntry.key);
|
||||
}
|
||||
|
||||
// 对比每种语言
|
||||
for (final entry in languageFiles.entries) {
|
||||
if (entry.key == 'zh_CN') continue;
|
||||
|
||||
final langId = entry.key;
|
||||
final filePath = '$languagesDir\\${entry.value}';
|
||||
final file = File(filePath);
|
||||
if (!file.existsSync()) continue;
|
||||
|
||||
final content = file.readAsStringSync();
|
||||
stdout.writeln('\n── $langId ──');
|
||||
|
||||
for (final typeEntry in typeFieldsMap.entries) {
|
||||
final typeName = typeEntry.key;
|
||||
final baseTypeFields = baseFields[typeName] ?? {};
|
||||
final langTypeFields = compositeTypes.contains(typeName)
|
||||
? parseLanguageTypeFieldsRecursive(content, typeName)
|
||||
: parseLanguageTypeFields(content, typeName);
|
||||
|
||||
final missing = <String>[];
|
||||
final empty = <String>[];
|
||||
final different = <String>[];
|
||||
|
||||
for (final baseField in baseTypeFields.entries) {
|
||||
final key = baseField.key;
|
||||
final baseValue = baseField.value;
|
||||
|
||||
if (!langTypeFields.containsKey(key)) {
|
||||
missing.add(key);
|
||||
} else if (langTypeFields[key]!.isEmpty) {
|
||||
empty.add(key);
|
||||
} else if (langTypeFields[key] != baseValue && baseValue.isNotEmpty) {
|
||||
different.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.isEmpty && empty.isEmpty) continue;
|
||||
|
||||
stdout.writeln(' [$typeName]');
|
||||
if (missing.isNotEmpty) {
|
||||
stdout.writeln(' ❌ 缺失 (${missing.length}): ${missing.join(', ')}');
|
||||
}
|
||||
if (empty.isNotEmpty) {
|
||||
stdout.writeln(' ⚠️ 空值 (${empty.length}): ${empty.join(', ')}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stdout.writeln('\n' + '=' * 60);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// --skeleton: 生成类型定义文件骨架
|
||||
int generateSkeleton() {
|
||||
stdout.writeln('🦴 类型定义骨架生成');
|
||||
stdout.writeln('=' * 60);
|
||||
|
||||
for (final typeFile in typeFiles) {
|
||||
if (typeFile == 't_root.dart') continue;
|
||||
final filePath = '$typesDir\\$typeFile';
|
||||
final typeName = getTypeName(typeFile);
|
||||
|
||||
final fields = parseTypeFields(filePath);
|
||||
if (fields.isEmpty) continue;
|
||||
|
||||
stdout.writeln('\n── $typeName (${fields.length} fields) ──');
|
||||
stdout.writeln();
|
||||
|
||||
// 构造函数
|
||||
stdout.writeln('const $typeName({');
|
||||
for (final field in fields.keys) {
|
||||
stdout.writeln(' required this.$field,');
|
||||
}
|
||||
stdout.writeln('});');
|
||||
|
||||
stdout.writeln();
|
||||
|
||||
// 字段声明
|
||||
for (final entry in fields.entries) {
|
||||
if (entry.value.isNotEmpty) {
|
||||
stdout.writeln('/// ${entry.value}');
|
||||
}
|
||||
stdout.writeln('final String ${entry.key};');
|
||||
}
|
||||
|
||||
stdout.writeln();
|
||||
|
||||
// toMap
|
||||
stdout.writeln('Map<String, String> toMap() => {');
|
||||
for (final field in fields.keys) {
|
||||
stdout.writeln(" '$field': $field,");
|
||||
}
|
||||
stdout.writeln('};');
|
||||
|
||||
stdout.writeln();
|
||||
|
||||
// fromMap
|
||||
stdout.writeln(
|
||||
'static $typeName fromMap(Map<String, String> map) => $typeName(',
|
||||
);
|
||||
for (final field in fields.keys) {
|
||||
stdout.writeln(" $field: map['$field'] ?? '',");
|
||||
}
|
||||
stdout.writeln(');');
|
||||
}
|
||||
|
||||
stdout.writeln('\n' + '=' * 60);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// --report: 输出完整覆盖率报告
|
||||
int fullReport() {
|
||||
stdout.writeln('📈 翻译覆盖率完整报告');
|
||||
stdout.writeln('=' * 60);
|
||||
|
||||
final baseFile = File(baseLanguageFile);
|
||||
if (!baseFile.existsSync()) {
|
||||
stderr.writeln('❌ 基准语言文件不存在: $baseLanguageFile');
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 解析类型文件
|
||||
final typeFieldsMap = <String, Map<String, String>>{};
|
||||
var totalFields = 0;
|
||||
for (final typeFile in typeFiles) {
|
||||
if (typeFile == 't_root.dart') continue;
|
||||
final filePath = '$typesDir\\$typeFile';
|
||||
final fields = parseTypeFields(filePath);
|
||||
if (fields.isNotEmpty) {
|
||||
final typeName = getTypeName(typeFile);
|
||||
typeFieldsMap[typeName] = fields;
|
||||
totalFields += fields.length;
|
||||
}
|
||||
}
|
||||
|
||||
stdout.writeln('\n总翻译字段数: $totalFields');
|
||||
stdout.writeln();
|
||||
|
||||
// 按模块统计
|
||||
stdout.writeln('── 模块字段统计 ──');
|
||||
for (final entry in typeFieldsMap.entries) {
|
||||
stdout.writeln(' ${entry.key}: ${entry.value.length} 个字段');
|
||||
}
|
||||
|
||||
stdout.writeln();
|
||||
|
||||
// 按语言统计
|
||||
stdout.writeln('── 语言覆盖率 ──');
|
||||
final results = <String, Map<String, dynamic>>{};
|
||||
|
||||
for (final entry in languageFiles.entries) {
|
||||
final langId = entry.key;
|
||||
final filePath = '$languagesDir\\${entry.value}';
|
||||
final file = File(filePath);
|
||||
|
||||
if (!file.existsSync()) {
|
||||
results[langId] = {'percent': 0, 'missing': totalFields};
|
||||
stdout.writeln(' ❌ $langId: 文件不存在');
|
||||
continue;
|
||||
}
|
||||
|
||||
final content = file.readAsStringSync();
|
||||
var missingCount = 0;
|
||||
var emptyCount = 0;
|
||||
|
||||
for (final typeEntry in typeFieldsMap.entries) {
|
||||
final typeName = typeEntry.key;
|
||||
final expectedFields = typeEntry.value;
|
||||
final langFields = compositeTypes.contains(typeName)
|
||||
? parseLanguageTypeFieldsRecursive(content, typeName)
|
||||
: parseLanguageTypeFields(content, typeName);
|
||||
|
||||
for (final fieldEntry in expectedFields.entries) {
|
||||
final fieldName = fieldEntry.key;
|
||||
if (!langFields.containsKey(fieldName)) {
|
||||
missingCount++;
|
||||
} else if (langFields[fieldName]!.isEmpty) {
|
||||
emptyCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final covered = totalFields - missingCount - emptyCount;
|
||||
final percent = totalFields > 0 ? (covered * 100 / totalFields).round() : 0;
|
||||
|
||||
results[langId] = {
|
||||
'percent': percent,
|
||||
'covered': covered,
|
||||
'missing': missingCount,
|
||||
'empty': emptyCount,
|
||||
};
|
||||
|
||||
final filled = percent ~/ 5;
|
||||
final bar = '█' * filled + '░' * (20 - filled);
|
||||
final status = percent >= 90
|
||||
? '✅'
|
||||
: percent >= 70
|
||||
? '⚠️'
|
||||
: '❌';
|
||||
|
||||
stdout.writeln(
|
||||
' $status $langId: $bar $percent% ($covered/$totalFields)'
|
||||
'${missingCount > 0 ? ' missing:$missingCount' : ''}'
|
||||
'${emptyCount > 0 ? ' empty:$emptyCount' : ''}',
|
||||
);
|
||||
}
|
||||
|
||||
// 机器翻译标注
|
||||
stdout.writeln();
|
||||
stdout.writeln('── 机器翻译标注 (覆盖率 < 80%) ──');
|
||||
final mtLangs = results.entries.where(
|
||||
(e) => (e.value['percent'] as int) < 80,
|
||||
);
|
||||
if (mtLangs.isEmpty) {
|
||||
stdout.writeln(' 无需标注 (所有语言覆盖率 >= 80%)');
|
||||
} else {
|
||||
for (final entry in mtLangs) {
|
||||
final percent = entry.value['percent'] as int;
|
||||
stdout.writeln(' 🤖 ${entry.key}: $percent% — 建议标注为机器翻译');
|
||||
}
|
||||
}
|
||||
|
||||
stdout.writeln('\n' + '=' * 60);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── 主入口 ────────────────────────────────────────────────────
|
||||
|
||||
void main(List<String> args) {
|
||||
if (args.isEmpty) {
|
||||
stdout.writeln('闲言APP — 翻译代码生成与检查工具');
|
||||
stdout.writeln();
|
||||
stdout.writeln('用法:');
|
||||
stdout.writeln(' dart run tools/gen_l10n.dart --check 检查所有语言文件的字段完整性');
|
||||
stdout.writeln(' dart run tools/gen_l10n.dart --diff 输出各语言与zh_cn的差异');
|
||||
stdout.writeln(' dart run tools/gen_l10n.dart --skeleton 生成类型定义文件骨架');
|
||||
stdout.writeln(' dart run tools/gen_l10n.dart --report 输出完整覆盖率报告');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
final command = args.first;
|
||||
|
||||
switch (command) {
|
||||
case '--check':
|
||||
exit(checkCompleteness());
|
||||
case '--diff':
|
||||
exit(diffLanguages());
|
||||
case '--skeleton':
|
||||
exit(generateSkeleton());
|
||||
case '--report':
|
||||
exit(fullReport());
|
||||
default:
|
||||
stderr.writeln('❌ 未知命令: $command');
|
||||
stderr.writeln('可用命令: --check, --diff, --skeleton, --report');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
# ============================================================
|
||||
# 闲言APP — pubspec.yaml 平台模板生成脚本
|
||||
# 创建时间: 2026-06-02
|
||||
# 更新时间: 2026-06-02
|
||||
# 作用: 根据平台选择模板生成 pubspec.yaml
|
||||
# 上次更新: 初始版本
|
||||
# 用法:
|
||||
# .\tools\setup_pubspec.ps1 -Platform ohos # 鸿蒙端
|
||||
# .\tools\setup_pubspec.ps1 -Platform macos # MacBook Pro端
|
||||
# .\tools\setup_pubspec.ps1 # 自动检测平台
|
||||
# ============================================================
|
||||
|
||||
param(
|
||||
[ValidateSet("ohos", "macos", "auto")]
|
||||
[string]$Platform = "auto"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$ProjectRoot = $PSScriptRoot
|
||||
if ($ProjectRoot) {
|
||||
$ProjectRoot = Split-Path -Parent $ProjectRoot
|
||||
}
|
||||
if (-not $ProjectRoot) {
|
||||
$ProjectRoot = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
if ($ProjectRoot) {
|
||||
$ProjectRoot = Split-Path -Parent $ProjectRoot
|
||||
}
|
||||
}
|
||||
if (-not $ProjectRoot) {
|
||||
$ProjectRoot = (Get-Location).Path
|
||||
}
|
||||
|
||||
$OhosTemplate = Join-Path $ProjectRoot "pubspec.ohos.yaml"
|
||||
$MacosTemplate = Join-Path $ProjectRoot "pubspec.macos.yaml"
|
||||
$OutputFile = Join-Path $ProjectRoot "pubspec.yaml"
|
||||
|
||||
function Write-Status {
|
||||
param([string]$Message)
|
||||
Write-Host "[setup_pubspec] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host "[setup_pubspec] WARNING: $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host "[setup_pubspec] ERROR: $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
# --- 自动检测平台 ---
|
||||
if ($Platform -eq "auto") {
|
||||
Write-Status "Auto-detecting platform..."
|
||||
|
||||
$flutterPath = Get-Command flutter -ErrorAction SilentlyContinue
|
||||
if ($flutterPath) {
|
||||
$flutterVersion = & flutter --version 2>&1 | Select-String -Pattern "ohos|HarmonyOS" -Quiet
|
||||
if ($flutterVersion) {
|
||||
$Platform = "ohos"
|
||||
Write-Status "Detected flutter-ohos SDK -> ohos"
|
||||
} else {
|
||||
$Platform = "macos"
|
||||
Write-Status "Detected official Flutter SDK -> macos"
|
||||
}
|
||||
} else {
|
||||
# 检查 packages 目录是否存在
|
||||
$packagesDir = Join-Path $ProjectRoot "packages"
|
||||
if (Test-Path $packagesDir) {
|
||||
$Platform = "ohos"
|
||||
Write-Status "Found packages/ directory -> ohos"
|
||||
} else {
|
||||
$Platform = "macos"
|
||||
Write-Status "No packages/ directory -> macos"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- 选择模板 ---
|
||||
$TemplateFile = switch ($Platform) {
|
||||
"ohos" { $OhosTemplate }
|
||||
"macos" { $MacosTemplate }
|
||||
}
|
||||
|
||||
if (-not (Test-Path $TemplateFile)) {
|
||||
Write-Error "Template file not found: $TemplateFile"
|
||||
Write-Error "Expected: pubspec.ohos.yaml or pubspec.macos.yaml in project root"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- 备份现有 pubspec.yaml ---
|
||||
if (Test-Path $OutputFile) {
|
||||
$BackupFile = Join-Path $ProjectRoot "pubspec.yaml.bak"
|
||||
Copy-Item $OutputFile $BackupFile -Force
|
||||
Write-Status "Backed up existing pubspec.yaml -> pubspec.yaml.bak"
|
||||
}
|
||||
|
||||
# --- 复制模板 ---
|
||||
Copy-Item $TemplateFile $OutputFile -Force
|
||||
|
||||
$PlatformLabel = switch ($Platform) {
|
||||
"ohos" { "鸿蒙端 (HarmonyOS)" }
|
||||
"macos" { "MacBook Pro端 (iOS/macOS)" }
|
||||
}
|
||||
|
||||
Write-Status "========================================"
|
||||
Write-Status "Generated pubspec.yaml for: $PlatformLabel"
|
||||
Write-Status "Template: $(Split-Path $TemplateFile -Leaf)"
|
||||
Write-Status "Output: pubspec.yaml"
|
||||
Write-Status "========================================"
|
||||
|
||||
# --- 验证关键差异 ---
|
||||
if ($Platform -eq "ohos") {
|
||||
$hasPathRef = Select-String -Path $OutputFile -Pattern "path: packages/" -Quiet
|
||||
if (-not $hasPathRef) {
|
||||
Write-Warning "ohos template has no 'path: packages/' references - check template!"
|
||||
}
|
||||
Write-Status "Next steps:"
|
||||
Write-Status " 1. flutter pub get"
|
||||
Write-Status " 2. flutter build hap (or your ohos build command)"
|
||||
} else {
|
||||
$hasPathRef = Select-String -Path $OutputFile -Pattern "path: packages/" -Quiet
|
||||
if ($hasPathRef) {
|
||||
Write-Warning "macos template still has 'path: packages/' references - check template!"
|
||||
}
|
||||
Write-Status "Next steps:"
|
||||
Write-Status " 1. flutter pub get"
|
||||
Write-Status " 2. Apply pub cache patches (see iOS_macOS_Developer_Guide.md section 2.6)"
|
||||
Write-Status " 3. flutter build ios --no-codesign"
|
||||
Write-Status " 4. flutter build macos"
|
||||
}
|
||||
|
||||
Write-Status ""
|
||||
Write-Status "NOTE: pubspec.yaml is in .gitignore and will NOT be committed."
|
||||
Write-Status "When adding a new dependency, update BOTH pubspec.ohos.yaml AND pubspec.macos.yaml"
|
||||
Write-Status "Then update iOS_macOS_Developer_Guide.md to notify the other platform developer."
|
||||
@@ -1,81 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
void main() {
|
||||
final files = <String, String>{
|
||||
't_settings_performance.dart': 'TSettingsPerformance',
|
||||
't_settings_privacy.dart': 'TSettingsPrivacy',
|
||||
't_settings_advanced.dart': 'TSettingsAdvanced',
|
||||
't_settings_cache.dart': 'TSettingsCache',
|
||||
't_settings_permission.dart': 'TSettingsPermission',
|
||||
't_settings_data_collection.dart': 'TSettingsDataCollection',
|
||||
};
|
||||
|
||||
const baseDir = 'e:\\project\\flutter\\f\\xianyan\\lib\\l10n\\types';
|
||||
|
||||
for (final entry in files.entries) {
|
||||
final file = File('$baseDir\\${entry.key}');
|
||||
final className = entry.value;
|
||||
if (!file.existsSync()) {
|
||||
print('NOT FOUND: ${entry.key}');
|
||||
continue;
|
||||
}
|
||||
|
||||
var content = file.readAsStringSync();
|
||||
|
||||
// Find the fromMap method
|
||||
final startPattern = 'static $className fromMap(Map<String, String> map)';
|
||||
final startIdx = content.indexOf(startPattern);
|
||||
if (startIdx == -1) {
|
||||
print('NO fromMap found: ${entry.key}');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the end of the method (matching closing );
|
||||
var depth = 0;
|
||||
var endIdx = startIdx;
|
||||
var foundOpen = false;
|
||||
for (var i = startIdx; i < content.length; i++) {
|
||||
if (content[i] == '(') {
|
||||
depth++;
|
||||
foundOpen = true;
|
||||
} else if (content[i] == ')') {
|
||||
depth--;
|
||||
}
|
||||
if (foundOpen && depth == 0) {
|
||||
// Find the semicolon
|
||||
endIdx = content.indexOf(';', i);
|
||||
if (endIdx == -1) endIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final oldMethod = content.substring(startIdx, endIdx + 1);
|
||||
|
||||
// Parse all field: map['key'] ?? '' patterns
|
||||
final fieldPattern = RegExp(r"(\w+):\s*map\['([^']+)'\]\s*\?\?\s*''");
|
||||
final matches = fieldPattern.allMatches(oldMethod);
|
||||
|
||||
final newLines = <String>[];
|
||||
for (final m in matches) {
|
||||
final fieldName = m.group(1)!;
|
||||
final mapKey = m.group(2)!;
|
||||
newLines.add(' $fieldName: map[\'$mapKey\']?.isNotEmpty == true');
|
||||
newLines.add(' ? map[\'$mapKey\']!');
|
||||
newLines.add(' : (fallback?.$fieldName ?? \'\'),');
|
||||
}
|
||||
|
||||
final newMethod =
|
||||
'static $className fromMap(Map<String, String> map,\n'
|
||||
' {$className? fallback}) =>\n'
|
||||
' $className(\n'
|
||||
'${newLines.join('\n')}\n'
|
||||
' );';
|
||||
|
||||
content =
|
||||
content.substring(0, startIdx) +
|
||||
newMethod +
|
||||
content.substring(endIdx + 1);
|
||||
file.writeAsStringSync(content);
|
||||
print('OK: ${entry.key} (${matches.length} fields)');
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 双书名号检测规则
|
||||
/// 创建时间: 2026-06-02
|
||||
/// 更新时间: 2026-06-02
|
||||
/// 作用: 检测字符串中的双书名号《《,预防多人协作风格不一致
|
||||
/// 上次更新: 初始创建
|
||||
/// ============================================================
|
||||
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/error/listener.dart';
|
||||
import 'package:custom_lint_builder/custom_lint_builder.dart';
|
||||
|
||||
class DoubleAngleBracketsRule extends DartLintRule {
|
||||
DoubleAngleBracketsRule() : super(code: _code);
|
||||
|
||||
static const _code = LintCode(
|
||||
name: 'double_angle_brackets',
|
||||
problemMessage: '检测到双书名号《《,应为单书名号《》',
|
||||
correctionMessage: '将《《替换为《,确保书名号正确配对',
|
||||
);
|
||||
|
||||
@override
|
||||
void run(
|
||||
CustomLintResolver resolver,
|
||||
DiagnosticReporter reporter,
|
||||
CustomLintContext context,
|
||||
) {
|
||||
context.registry.addSimpleStringLiteral((node) {
|
||||
_check(node.value, node, reporter);
|
||||
});
|
||||
|
||||
context.registry.addStringInterpolation((node) {
|
||||
for (final element in node.elements) {
|
||||
if (element is InterpolationString) {
|
||||
_check(element.value, element, reporter);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _check(String value, AstNode node, DiagnosticReporter reporter) {
|
||||
if (value.contains('《《')) {
|
||||
reporter.atNode(node, _code);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 硬编码颜色检测规则
|
||||
/// 创建时间: 2026-06-02
|
||||
/// 更新时间: 2026-06-02
|
||||
/// 作用: 检测非主题系统的硬编码颜色值,确保使用统一设计令牌
|
||||
/// 上次更新: 初始创建
|
||||
/// ============================================================
|
||||
|
||||
import 'package:analyzer/error/listener.dart';
|
||||
import 'package:custom_lint_builder/custom_lint_builder.dart';
|
||||
|
||||
class HardcodedColorRule extends DartLintRule {
|
||||
HardcodedColorRule() : super(code: _code);
|
||||
|
||||
static const _code = LintCode(
|
||||
name: 'hardcoded_color',
|
||||
problemMessage: '检测到硬编码颜色值,应使用主题系统变量',
|
||||
correctionMessage: '使用 AppTheme.ext(context) 获取颜色,或添加到 app_colors.dart',
|
||||
);
|
||||
|
||||
static final _hexColorPattern = RegExp(r'0x[0-9A-Fa-f]{8}');
|
||||
|
||||
static const _excludedFiles = <String>[
|
||||
'app_colors.dart',
|
||||
'app_theme.dart',
|
||||
'color_weak_filter.dart',
|
||||
'glass_tokens.dart',
|
||||
'app_radius.dart',
|
||||
'app_shadow.dart',
|
||||
];
|
||||
|
||||
@override
|
||||
void run(
|
||||
CustomLintResolver resolver,
|
||||
DiagnosticReporter reporter,
|
||||
CustomLintContext context,
|
||||
) {
|
||||
final filePath = resolver.path;
|
||||
if (_excludedFiles.any((e) => filePath.contains(e))) return;
|
||||
|
||||
context.registry.addInstanceCreationExpression((node) {
|
||||
final typeName = node.constructorName.type.name.lexeme;
|
||||
if (typeName != 'Color') return;
|
||||
|
||||
for (final arg in node.argumentList.arguments) {
|
||||
final argStr = arg.toSource();
|
||||
if (_hexColorPattern.hasMatch(argStr)) {
|
||||
reporter.atNode(arg, _code);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 自定义Lint规则入口
|
||||
/// 创建时间: 2026-06-02
|
||||
/// 更新时间: 2026-06-02
|
||||
/// 作用: 注册所有自定义lint规则插件
|
||||
/// 上次更新: 移除硬编码中文检测规则
|
||||
/// ============================================================
|
||||
|
||||
import 'package:custom_lint_builder/custom_lint_builder.dart';
|
||||
|
||||
import 'src/rules/double_angle_brackets.dart';
|
||||
import 'src/rules/hardcoded_color.dart';
|
||||
|
||||
PluginBase createPlugin() => _XianyanLintPlugin();
|
||||
|
||||
class _XianyanLintPlugin extends PluginBase {
|
||||
@override
|
||||
List<LintRule> getLintRules(CustomLintConfigs configs) => [
|
||||
DoubleAngleBracketsRule(),
|
||||
HardcodedColorRule(),
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user