Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog


### [3.0.9]

- added .yaml support for transaltion files in the :generate script
Comment thread
Lucas-Feichtinger marked this conversation as resolved.
Outdated

### [3.0.8]

- code audit and maintenance updates
Expand Down
64 changes: 43 additions & 21 deletions bin/generate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:io';

import 'package:args/args.dart';
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';

const _preservedKeywords = [
'few',
Expand Down Expand Up @@ -70,8 +71,8 @@ ArgParser _generateArgParser(GenerateOptions? generateOptions) {
abbr: 'f',
defaultsTo: 'json',
callback: (String? x) => generateOptions!.format = x,
help: 'Support json or keys formats',
allowed: ['json', 'keys']);
help: 'Support json, yaml, or keys formats',
allowed: ['json', 'yaml', 'keys']);

parser.addFlag(
'skip-unnecessary-keys',
Expand Down Expand Up @@ -122,7 +123,7 @@ void handleLangFiles(GenerateOptions options) async {
files = [sourceFile];
} else {
//filtering format
files = files.where((f) => f.path.contains('.json')).toList();
files = files.where((f) => f.path.contains(RegExp(r'\.(json|yaml|yml)$'))).toList();
}
Comment on lines 125 to 127
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Filter by selected format and ensure only files are processed.
Currently mixes JSON with YAML when format='yaml' and may include directories. Make filtering deterministic and case-insensitive.

-    //filtering format
-    files = files.where((f) => f.path.contains(RegExp(r'\.(json|yaml|yml)$'))).toList();
+    // Filter files based on requested format; keep only regular files.
+    files = files
+        .whereType<File>()
+        .where((f) {
+          final p = f.path.toLowerCase();
+          switch (options.format) {
+            case 'json':
+              return p.endsWith('.json');
+            case 'yaml':
+              return p.endsWith('.yaml') || p.endsWith('.yml');
+            case 'keys':
+              return p.endsWith('.json') || p.endsWith('.yaml') || p.endsWith('.yml');
+            default:
+              return false;
+          }
+        })
+        .toList();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//filtering format
files = files.where((f) => f.path.contains('.json')).toList();
files = files.where((f) => f.path.contains(RegExp(r'\.(json|yaml|yml)$'))).toList();
}
// Filter files based on requested format; keep only regular files.
files = files
.whereType<File>()
.where((f) {
final p = f.path.toLowerCase();
switch (options.format) {
case 'json':
return p.endsWith('.json');
case 'yaml':
return p.endsWith('.yaml') || p.endsWith('.yml');
case 'keys':
return p.endsWith('.json') || p.endsWith('.yaml') || p.endsWith('.yml');
default:
return false;
}
})
.toList();
}
🤖 Prompt for AI Agents
In bin/generate.dart around lines 125-127, the current filter mixes JSON and
YAML when format='yaml' and can include directories; update the filtering to
first exclude non-files using FileSystemEntity.isFileSync(path) and then match
extensions deterministically: build the allowed extensions based on the selected
format (e.g., ['json'] for 'json', ['yaml','yml'] for 'yaml'), compile a RegExp
that matches the extension at the end of the path with caseSensitive: false, and
filter files by both being a real file and matching that regex.


if (files.isNotEmpty) {
Expand Down Expand Up @@ -154,12 +155,12 @@ void generateFile(List<FileSystemEntity> files, Directory outputPath,
case 'json':
await _writeJson(classBuilder, files);
break;
case 'yaml':
await _writeYaml(classBuilder, files);
break;
case 'keys':
await _writeKeys(classBuilder, files, options.skipUnnecessaryKeys);
break;
// case 'csv':
// await _writeCsv(classBuilder, files);
// break;
default:
stderr.writeln('Format not supported');
}
Expand All @@ -183,7 +184,7 @@ abstract class LocaleKeys {
final fileData = File(files.first.path);

Map<String, dynamic> translations =
json.decode(await fileData.readAsString());
json.decode(json.encode(loadYaml(await fileData.readAsString())));

Comment on lines 187 to 191
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Keys generation reads YAML only — add shared parser to support JSON and validate top-level.

Without this, -f keys fails for .json sources and lacks structure checks.

-  Map<String, dynamic> translations =
-      json.decode(json.encode(loadYaml(await fileData.readAsString())));
+  final Map<String, dynamic> translations = await _parseTranslations(fileData);

Add this helper (place near _resolve):

// Parses .json, .yaml, .yml into Map<String, dynamic> and validates top-level map.
Future<Map<String, dynamic>> _parseTranslations(File file) async {
  final ext = path.extension(file.path).toLowerCase();
  final content = await file.readAsString();
  if (ext == '.json') {
    final decoded = json.decode(content);
    if (decoded is! Map) {
      throw const FormatException('Top-level JSON must be an object');
    }
    return Map<String, dynamic>.from(decoded as Map);
  }
  // YAML path
  final yamlRoot = loadYaml(content);
  // Normalize YamlMap/YamlList into JSON-friendly Map/List
  final normalized = json.decode(json.encode(yamlRoot));
  if (normalized is! Map) {
    throw const FormatException('Top-level YAML must be a mapping');
  }
  return Map<String, dynamic>.from(normalized as Map);
}
🤖 Prompt for AI Agents
In bin/generate.dart around lines 187 to 191, the current code reads YAML only
and directly decodes a YAML structure into a Map which breaks when input is JSON
and does not validate top-level structure; add a helper method named
_parseTranslations placed near _resolve that: reads the file bytes, inspects
path.extension(file.path).toLowerCase(), for '.json' uses json.decode and throws
a FormatException if the decoded value is not a Map, for YAML ('.yaml' or
'.yml') uses loadYaml then normalizes the YamlMap/YamlList to JSON-friendly
structures via json.encode/json.decode, validates the normalized value is a Map,
converts and returns Map<String, dynamic>; then replace the current YAML-only
logic at lines 187–191 to call await _parseTranslations(fileData) and use its
returned Map.

file += _resolve(translations, skipUnnecessaryKeys);

Expand Down Expand Up @@ -271,22 +272,43 @@ class CodegenLoader extends AssetLoader{
classBuilder.writeln(gFile);
}

// _writeCsv(StringBuffer classBuilder, List<FileSystemEntity> files) async {
// List<String> listLocales = List();
// final fileData = File(files.first.path);
Future _writeYaml(
StringBuffer classBuilder, List<FileSystemEntity> files) async {
var gFile = '''
// DO NOT EDIT. This is code generated via package:easy_localization/generate.dart

// ignore_for_file: prefer_single_quotes, avoid_renaming_method_parameters, constant_identifier_names

import 'dart:ui';

import 'package:easy_localization/easy_localization.dart' show AssetLoader;

class CodegenLoader extends AssetLoader{
const CodegenLoader();

@override
Future<Map<String, dynamic>?> load(String path, Locale locale) {
return Future.value(mapLocales[locale.toString()]);
}

// // CSVParser csvParser = CSVParser(await fileData.readAsString());
''';

// // List listLangs = csvParser.getLanguages();
// for(String localeName in listLangs){
// listLocales.add('"$localeName": $localeName');
// String mapString = JsonEncoder.withIndent(" ").convert(csvParser.getLanguageMap(localeName)) ;
final listLocales = [];

// classBuilder.writeln(
// ' static const Map<String,dynamic> $localeName = ${mapString};\n');
// }
for (var file in files) {
final localeName = path
.basename(file.path)
.replaceFirst(RegExp(r'\.(yaml|yml)'), '')
.replaceAll('-', '_');
listLocales.add('"$localeName": _$localeName');
final fileData = File(file.path);

// classBuilder.writeln(
// ' static const Map<String, Map<String,dynamic>> mapLocales = \{${listLocales.join(', ')}\};');
var data = loadYaml(await fileData.readAsString());
final mapString = const JsonEncoder.withIndent(' ').convert(data);
gFile += 'static const Map<String,dynamic> _$localeName = $mapString;\n';
}

// }
gFile +=
'static const Map<String, Map<String,dynamic>> mapLocales = {${listLocales.join(', ')}};';
classBuilder.writeln(gFile);
}
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies:
intl: '>=0.17.0-0 <0.21.0'
args: ^2.3.1
path: ^1.8.1
yaml: ^3.1.2
easy_logger: ^0.0.2
flutter_localizations:
sdk: flutter
Expand Down