Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 translation files in the :generate script

### [3.0.8]

- code audit and maintenance updates
Expand Down
53 changes: 49 additions & 4 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

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,6 +155,9 @@ 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;
Expand Down Expand Up @@ -183,7 +187,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

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,6 +275,47 @@ class CodegenLoader extends AssetLoader{
classBuilder.writeln(gFile);
}

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()]);
}

''';

final listLocales = [];

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);

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);
}

// _writeCsv(StringBuffer classBuilder, List<FileSystemEntity> files) async {
// List<String> listLocales = List();
// final fileData = File(files.first.path);
Expand Down
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