Skip to content

Commit

Permalink
feat: simplify and normalize file names
Browse files Browse the repository at this point in the history
  • Loading branch information
Tienisto committed Nov 29, 2024
1 parent e25d4f7 commit acecaa2
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 36 deletions.
10 changes: 10 additions & 0 deletions slang/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 4.3.0

- feat: simplify file names without namespaces (#267)
- **DEPRECATED:** Do not use namespaces in file names when namespaces are disabled: `strings_de.json` -> `de.json`
- **DEPRECATED:** Always specify the locale in the file name (namespace enabled): `strings.json` -> `strings_en.json`, except the locale is specified in the directory name

Note: This might make the files order in your IDE less pleasant if input and output files are in the same directory.
You can specify the output directory to a subdirectory to avoid this.
For example, `output_directory: lib/i18n/gen`

## 4.2.1

- fix: do not sanitize keys in maps
Expand Down
44 changes: 44 additions & 0 deletions slang/MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
# Migration Guides

## slang 4.0 to 5.0

### Remove namespace in file name

When namespaces are disabled, remove the namespace in the file name.

Before:
```text
lib/
└── i18n/
└── strings.i18n.json
└── strings_de.i18n.json
```

After:
```text
lib/
└── i18n/
└── en.i18n.json
└── de.i18n.json
```

### Add locale in file name

When namespaces are enabled, always specify the locale in the file name.

Before:
```text
i18n/
└── widgets.i18n.json
└── widgets_fr.i18n.json
└── errorDialogs.i18n.json
└── errorDialogs_fr.i18n.json
```

After:
```text
i18n/
└── widgets_en.i18n.json
└── widgets_fr.i18n.json
└── errorDialogs_en.i18n.json
└── errorDialogs_fr.i18n.json
```

## slang 3.0 to 4.0

### Lazy Loading
Expand Down
20 changes: 9 additions & 11 deletions slang/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,24 +133,22 @@ dev_dependencies:
Format:
```text
<namespace>_<locale?>.<extension>
<locale>.<extension>
```

You can ignore the [namespace](#-namespaces) for this basic example, so just use a generic name like `strings`.

Most common i18n directories are `assets/i18n` and `lib/i18n`. (see [Assets](#-assets)).

Example:
```text
lib/
└── i18n/
└── strings.i18n.json
└── strings_de.i18n.json
└── strings_zh-CN.i18n.json <-- example for country code
└── en.i18n.json
└── de.i18n.json
└── zh-CN.i18n.json <-- example for country code
```

```json5
// File: strings.i18n.json (mandatory, base locale)
// File: en.i18n.json
{
"hello": "Hello $name",
"save": "Save",
Expand All @@ -162,7 +160,7 @@ lib/
```

```json5
// File: strings_de.i18n.json
// File: de.i18n.json
{
"hello": "Hallo $name",
"save": "Speichern",
Expand Down Expand Up @@ -1220,14 +1218,14 @@ output_file_name: translations.g.dart # set file name (mandatory)
Let's create two namespaces called `widgets` and `errorDialogs`. Please use camel case for multiple words.

```text
<namespace>_<locale?>.<extension>
<namespace>_<locale>.<extension>
```

```text
i18n/
└── widgets.i18n.json
└── widgets_en.i18n.json
└── widgets_fr.i18n.json
└── errorDialogs.i18n.json <-- camel case for multiple words
└── errorDialogs_en.i18n.json <-- camel case for multiple words
└── errorDialogs_fr.i18n.json
```

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
113 changes: 90 additions & 23 deletions slang/lib/src/builder/builder/slang_file_collection_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:io';

import 'package:collection/collection.dart';
import 'package:slang/src/builder/builder/raw_config_builder.dart';
import 'package:slang/src/builder/model/enums.dart';
import 'package:slang/src/builder/model/i18n_locale.dart';
import 'package:slang/src/builder/model/raw_config.dart';
import 'package:slang/src/builder/model/slang_file_collection.dart';
Expand Down Expand Up @@ -71,7 +72,9 @@ class SlangFileCollectionBuilder {
static SlangFileCollection fromFileModel({
required RawConfig config,
required Iterable<PlainTranslationFile> files,
bool showWarning = true,
}) {
// True, if (1) namespaces are enabled and (2) directory locale is used
final includeUnderscore = config.namespaces &&
files.any((f) {
final fileNameNoExtension = PathUtils.getFileNameNoExtension(f.path);
Expand All @@ -95,11 +98,31 @@ class SlangFileCollectionBuilder {
.map((f) {
final fileNameNoExtension =
PathUtils.getFileNameNoExtension(f.path);

if (!config.namespaces) {
final localeMatch =
RegexUtils.localeRegex.firstMatch(fileNameNoExtension);
if (localeMatch != null) {
final locale = I18nLocale(
language: localeMatch.group(1)!,
script: localeMatch.group(2),
country: localeMatch.group(3),
);

return TranslationFile(
path: f.path,
locale: locale,
namespace: TranslationFile.DEFAULT_NAMESPACE,
read: f.read,
);
}
}

final baseFileMatch =
RegexUtils.baseFileRegex.firstMatch(fileNameNoExtension);
if (includeUnderscore || baseFileMatch != null) {
// base file (file without locale, may be multiples due to namespaces!)
// could also be a non-base locale when directory name is a locale
// base file (file without locale)
// could also be a non-base locale when directory name is a locale (namespace only)

// directory name could be a locale
I18nLocale? directoryLocale;
Expand All @@ -108,41 +131,67 @@ class SlangFileCollectionBuilder {
filePath: f.path,
inputDirectory: config.inputDirectory,
);

if (showWarning && directoryLocale == null) {
_baseLocaleDeprecationWarning(
fileName: PathUtils.getFileName(f.path),
replacement:
'${fileNameNoExtension}_${config.baseLocale.languageTag.replaceAll('-', '_')}${config.inputFilePattern}',
);
}
}

if (showWarning &&
!config.namespaces &&
config.fileType != FileType.csv) {
// Note: Compact CSV files are still allowed to have a file name without locale.
_namespaceDeprecationWarning(
fileName: PathUtils.getFileName(f.path),
replacement:
'${config.baseLocale.languageTag.replaceAll('-', '_')}${config.inputFilePattern}',
);
}

return TranslationFile(
path: f.path,
locale: directoryLocale ?? config.baseLocale,
namespace: fileNameNoExtension,
namespace: config.namespaces
? fileNameNoExtension
: TranslationFile.DEFAULT_NAMESPACE,
read: f.read,
);
} else {
// secondary files (strings_x)
final match = RegexUtils.fileWithLocaleRegex
.firstMatch(fileNameNoExtension);
if (match != null) {
final namespace = match.group(1)!;
final locale = I18nLocale(
language: match.group(2)!,
script: match.group(3),
country: match.group(4),
);
}

return TranslationFile(
path: f.path,
locale: locale,
namespace: namespace,
read: f.read,
// secondary files (strings_x)
final match =
RegexUtils.fileWithLocaleRegex.firstMatch(fileNameNoExtension);
if (match != null) {
final namespace = match.group(1)!;
final locale = I18nLocale(
language: match.group(2)!,
script: match.group(3),
country: match.group(4),
);

if (showWarning && !config.namespaces) {
_namespaceDeprecationWarning(
fileName: PathUtils.getFileName(f.path),
replacement:
'${locale.languageTag.replaceAll('-', '_')}${config.inputFilePattern}',
);
}

return TranslationFile(
path: f.path,
locale: locale,
namespace: namespace,
read: f.read,
);
}

return null;
})
// We cannot use "nonNulls" because this requires Dart 3.0
// and slang currently supports Dart 2.17
// ignore: deprecated_member_use
.whereNotNull()
.nonNulls
.sortedBy((file) => '${file.locale}-${file.namespace}'),
);
}
Expand Down Expand Up @@ -244,3 +293,21 @@ extension on String {
return PathUtils.getFileName(this);
}
}

void _namespaceDeprecationWarning({
required String fileName,
required String replacement,
}) {
print(
'DEPRECATED(v4.3.0): Do not use namespaces in file names when namespaces are disabled: "$fileName" -> "$replacement"',
);
}

void _baseLocaleDeprecationWarning({
required String fileName,
required String replacement,
}) {
print(
'DEPRECATED(v4.3.0): Always specify locale: "$fileName" -> "$replacement"',
);
}
2 changes: 2 additions & 0 deletions slang/lib/src/builder/model/slang_file_collection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class SlangFileCollection {
}

class TranslationFile extends PlainTranslationFile {
static const DEFAULT_NAMESPACE = r'$default$';

/// The inferred locale of this file (by file name, directory name, or config)
final I18nLocale locale;

Expand Down
31 changes: 29 additions & 2 deletions slang/test/unit/builder/slang_file_collection_builder_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,45 @@ PlainTranslationFile _file(String path) {

void main() {
group('SlangFileCollectionBuilder.fromFileModel', () {
test('should find base locale', () {
test('should find locales', () {
final model = SlangFileCollectionBuilder.fromFileModel(
config: RawConfig.defaultConfig
.copyWith(baseLocale: I18nLocale(language: 'de')),
files: [
_file('lib/i18n/en.i18n.json'),
_file('lib/i18n/de.i18n.json'),
_file('lib/i18n/fr_FR.i18n.json'),
_file('lib/i18n/zh-CN.i18n.json'),
],
);

expect(model.files.length, 4);
expect(
model.files.map((f) => f.locale.languageTag).toList(),
[
'de',
'en',
'fr-FR',
'zh-CN',
],
);
});

test('should find base locale (legacy)', () {
final model = SlangFileCollectionBuilder.fromFileModel(
config: RawConfig.defaultConfig
.copyWith(baseLocale: I18nLocale(language: 'de')),
files: [
_file('lib/i18n/strings.i18n.json'),
],
showWarning: false,
);

expect(model.files.length, 1);
expect(model.files.first.locale.language, 'de');
});

test('should find locale in file names', () {
test('should find locale in file names (legacy)', () {
final model = SlangFileCollectionBuilder.fromFileModel(
config: RawConfig.defaultConfig
.copyWith(baseLocale: I18nLocale(language: 'en')),
Expand All @@ -32,6 +57,7 @@ void main() {
_file('lib/i18n/strings_de.i18n.json'),
_file('lib/i18n/strings-fr-FR.i18n.json'),
],
showWarning: false,
);

expect(model.files.length, 3);
Expand All @@ -50,6 +76,7 @@ void main() {
files: [
_file('lib/i18n/dialogs.i18n.json'),
],
showWarning: false,
);

expect(model.files.length, 1);
Expand Down

0 comments on commit acecaa2

Please sign in to comment.