Skip to content
Merged
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
11 changes: 8 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ insert_final_newline = true
trim_trailing_whitespace = true

# Markdown files
[*.{md}]
[*.md]
end_of_line = crlf
trim_trailing_whitespace = false

Expand All @@ -39,7 +39,6 @@ indent_size = 2
# Json files
[*.json]
end_of_line = crlf
indent_size = 4

# Linux scripts
[*.sh]
Expand Down Expand Up @@ -73,7 +72,7 @@ csharp_prefer_simple_using_statement = true
csharp_prefer_static_anonymous_function = true
csharp_prefer_static_local_function = true
csharp_prefer_system_threading_lock = true
csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
csharp_preferred_modifier_order = public,private,protected,internal,file,static,abstract,sealed,virtual,override,readonly,unsafe,volatile,async,extern,new,partial:warning
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = false
csharp_space_after_cast = false
Expand Down Expand Up @@ -204,3 +203,9 @@ dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
dotnet_style_readonly_field = true
dotnet_style_require_accessibility_modifiers = for_non_interface_members

# ReSharper settings
resharper_csharp_trailing_comma_in_multiline_lists = true
resharper_csharp_var_for_built_in_types = false
resharper_csharp_var_when_type_is_apparent = false
resharper_csharp_var_when_type_is_not_apparent = false
114 changes: 64 additions & 50 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@
- Converts to JSON and generates C# code files
- Target framework: .NET 10.0

- **LanguageTagsTests** (`LanguageTagsTests/LanguageTagsTests.csproj`)
- xUnit test suite with 211+ comprehensive tests
- Uses AwesomeAssertions for test assertions
- 100% coverage of all public APIs
- Target framework: .NET 10.0
- **LanguageTagsTests** (`LanguageTagsTests/LanguageTagsTests.csproj`)
- xUnit test suite with comprehensive coverage
- Uses AwesomeAssertions for test assertions
- Target framework: .NET 10.0

### Key Directories

Expand All @@ -56,12 +55,9 @@ The main public API for working with language tags:

**Static Factory Methods:**
- `Parse(string tag)`: Parse a language tag string, returns null on failure
- `Parse(string tag, Options? options)`: Parse with per-call logging options
- `TryParse(string tag, out LanguageTag? result)`: Safe parsing with out parameter
- `TryParse(string tag, out LanguageTag? result, Options? options)`: Safe parsing with per-call logging options
- `ParseOrDefault(string tag, LanguageTag? defaultTag = null)`: Parse with fallback to "und"
- `ParseAndNormalize(string tag)`: Parse and normalize in one step
- `ParseAndNormalize(string tag, Options? options)`: Parse and normalize with per-call logging options
- `CreateBuilder()`: Create a fluent builder instance
- `FromLanguage(string language)`: Factory for simple language tags
- `FromLanguageRegion(string language, string region)`: Factory for language+region tags
Expand All @@ -77,10 +73,9 @@ The main public API for working with language tags:
- `PrivateUse`: PrivateUseTag object
- `IsValid`: Property to check if tag is valid

**Instance Methods:**
- `Validate()`: Verify tag correctness
- `Normalize()`: Return normalized copy of tag
- `Normalize(Options? options)`: Return normalized copy with per-call logging options
**Instance Methods:**
- `Validate()`: Verify structural correctness
- `Normalize()`: Return normalized copy of tag (does not validate)
- `ToString()`: String representation
- `Equals()`: Equality comparison (case-insensitive)
- `GetHashCode()`: Hash code for collections
Expand All @@ -107,8 +102,7 @@ Fluent builder for constructing language tags:
- `PrivateUseAdd(string value)`: Add private use tag
- `PrivateUseAddRange(IEnumerable<string> values)`: Add multiple private use tags
- `Build()`: Return constructed tag (no validation)
- `Normalize()`: Return normalized tag (with validation)
- `Normalize(Options? options)`: Return normalized tag with per-call logging options
- `Normalize()`: Return normalized tag (no validation)

### LanguageTagParser Class (LanguageTagParser.cs)

Expand Down Expand Up @@ -138,56 +132,77 @@ Provides language code conversion and matching:
- `GetIsoFromIetf(string languageTag)`: Convert IETF to ISO format
- `IsMatch(string prefix, string languageTag)`: Prefix matching for content selection

**Construction:**
- `new LanguageLookup(Options? options = null)`: Optional per-instance logging
### LogOptions Class (LogOptions.cs)

Static class for configuring global logging for the entire library:

**Properties:**
- `LoggerFactory`: Gets or sets the global logger factory for creating category loggers

**Methods:**
- `SetFactory(ILoggerFactory loggerFactory)`: Configure the library to use a logger factory
- `TrySetFactory(ILoggerFactory loggerFactory)`: Set factory only if none is configured

**Logger Resolution Priority:**
1. `LoggerFactory` property (when not `NullLoggerFactory`)
2. `NullLogger.Instance` (default fallback)

**Important Notes:**
- Loggers are created and cached at time of use by each class instance
- Changes to `LoggerFactory` after a logger is created do not affect existing cached loggers
- Only new logger requests use updated configuration

### Data Models

#### Iso6392Data.cs
- ISO 639-2 language codes (3-letter bibliographic/terminologic codes)
- **Public Methods:**
- `Create()`: Load embedded data
- `LoadDataAsync(string fileName)`: Load from file
- `LoadJsonAsync(string fileName)`: Load from JSON
- `Find(string? languageTag, bool includeDescription)`: Find record by tag
- `Find(string? languageTag, bool includeDescription, Options? options)`: Find record by tag with logging options
#### Iso6392Data.cs
- ISO 639-2 language codes (3-letter bibliographic/terminologic codes)
- **Public Methods:**
- `Create()`: Load embedded data
- `FromDataAsync(string fileName)`: Load from file
- `FromJsonAsync(string fileName)`: Load from JSON
- `Find(string? languageTag, bool includeDescription)`: Find record by tag
- **Internal Methods:** `SaveJsonAsync(string fileName)`, `SaveCodeAsync(string fileName)`
- **Record Properties:** `Part2B`, `Part2T`, `Part1`, `RefName`

#### Iso6393Data.cs
- ISO 639-3 language codes (comprehensive language codes)
- **Public Methods:**
- `Create()`: Load embedded data
- `LoadDataAsync(string fileName)`: Load from file
- `LoadJsonAsync(string fileName)`: Load from JSON
- `Find(string? languageTag, bool includeDescription)`: Find record by tag
- `Find(string? languageTag, bool includeDescription, Options? options)`: Find record by tag with logging options
#### Iso6393Data.cs
- ISO 639-3 language codes (comprehensive language codes)
- **Public Methods:**
- `Create()`: Load embedded data
- `FromDataAsync(string fileName)`: Load from file
- `FromJsonAsync(string fileName)`: Load from JSON
- `Find(string? languageTag, bool includeDescription)`: Find record by tag
- **Internal Methods:** `SaveJsonAsync(string fileName)`, `SaveCodeAsync(string fileName)`
- **Record Properties:** `Id`, `Part2B`, `Part2T`, `Part1`, `Scope`, `LanguageType`, `RefName`, `Comment`

#### Rfc5646Data.cs
- RFC 5646 / BCP 47 language subtag registry
- **Public Methods:**
- `Create()`: Load embedded data
- `LoadDataAsync(string fileName)`: Load from file
- `LoadJsonAsync(string fileName)`: Load from JSON
- `Find(string? languageTag, bool includeDescription)`: Find record by tag
- `Find(string? languageTag, bool includeDescription, Options? options)`: Find record by tag with logging options
- **Properties:** `FileDate`, `RecordList`
- **Record Properties:** `Type`, `Tag`, `SubTag`, `Description` (ImmutableArray), `Added`, `SuppressScript`, `Scope`, `MacroLanguage`, `Deprecated`, `Comments` (ImmutableArray), `Prefix` (ImmutableArray), `PreferredValue`, `TagAny`
#### Rfc5646Data.cs
- RFC 5646 / BCP 47 language subtag registry
- **Public Methods:**
- `Create()`: Load embedded data
- `FromDataAsync(string fileName)`: Load from file
- `FromJsonAsync(string fileName)`: Load from JSON
- `Find(string? languageTag, bool includeDescription)`: Find record by tag
- **Properties:** `FileDate`, `RecordList`
- **Internal Methods:** `SaveJsonAsync(string fileName)`, `SaveCodeAsync(string fileName)`
- **Record Properties:** `Type`, `Tag`, `SubTag`, `Description` (ImmutableArray), `Added`, `SuppressScript`, `Scope`, `MacroLanguage`, `Deprecated`, `Comments` (ImmutableArray), `Prefix` (ImmutableArray), `PreferredValue`, `TagValue`
- **Enums:**
- `RecordType`: None, Language, ExtLanguage, Script, Variant, Grandfathered, Region, Redundant
- `RecordScope`: None, MacroLanguage, Collection, Special, PrivateUse

#### Supporting Classes

**ExtensionTag:**
**ExtensionTag (sealed record):**
- `Prefix`: Single-character extension prefix (char)
- `Tags`: ImmutableArray of extension values
- `ToString()`: Format as "prefix-tag1-tag2"
- `Normalize()`: Returns normalized copy with sorted, lowercase tags
- `Equals()`: Case-insensitive equality comparison

**PrivateUseTag:**
**PrivateUseTag (sealed record):**
- `Prefix`: Constant 'x'
- `Tags`: ImmutableArray of private use values
- `ToString()`: Format as "x-tag1-tag2"
- `Normalize()`: Returns normalized copy with sorted, lowercase tags
- `Equals()`: Case-insensitive equality comparison

### Language Tag Structure

Expand Down Expand Up @@ -273,24 +288,24 @@ LanguageTag tag = LanguageTag.ParseOrDefault(input); // Falls back to "und"

## Recent API Changes

### Changed (Breaking)
### Changed (Breaking)
- `LanguageTagParser` is now internal (use `LanguageTag.Parse()` instead)
- Properties changed from `IList<string>` to `ImmutableArray<string>`:
- `VariantList` → `Variants`
- `ExtensionList` → `Extensions`
- `TagList` → `Tags`
- Data file APIs are async-only: `LoadDataAsync()`/`LoadJsonAsync()`; sync versions removed
- Data file APIs are async-only and use static creators: `FromDataAsync()`/`FromJsonAsync()`
- Logging configuration now uses `ILoggerFactory` only; `ILogger` support was removed from `LogOptions`
- Tag construction requires use of factory methods or builder (constructors are internal)

### Added (Non-Breaking)
- `LanguageTag.ParseOrDefault()`: Safe parsing with fallback
- `LanguageTag.ParseAndNormalize()`: Combined parse and normalize
- `LanguageTag.ParseAndNormalize(string, Options?)`: Combined parse and normalize with logging options
- `LanguageTag.IsValid`: Property for validation
- `LanguageTag.FromLanguage()`, `FromLanguageRegion()`, `FromLanguageScriptRegion()`: Factory methods
- `IEquatable<LanguageTag>` implementation with operators
- Options-aware logging for parsing/normalization and lookup (`Options` + `LogOptions`)
- `LanguageLookup` supports optional logging via primary constructor
- `LogOptions` static class for global logging configuration with `ILoggerFactory`
- `ExtensionTag` and `PrivateUseTag` are now sealed records with `Normalize()` and case-insensitive `Equals()` methods
- Comprehensive XML documentation for all public APIs

## Future Improvements
Expand All @@ -300,7 +315,6 @@ Consider these areas for enhancement:
- Implement comprehensive subtag content validation against registry data
- Add more language lookup and validation features
- Improve error messages and diagnostics
- Consider making `ExtensionTag` and `PrivateUseTag` immutable records

## Contributing

Expand Down
23 changes: 12 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,21 @@ For comprehensive coding standards and detailed conventions, refer to [`.github/

### Key Components

**Public API Classes:**

- `LanguageTag`: Main class for working with language tags (parse, build, normalize, validate)
- `LanguageTagBuilder`: Fluent builder for constructing language tags
- `LanguageLookup`: Language code conversion and matching (IETF ↔ ISO)
- `Iso6392Data`: ISO 639-2 language code data
- `Iso6393Data`: ISO 639-3 language code data
- `Rfc5646Data`: RFC 5646 / BCP 47 language subtag registry data
- `ExtensionTag`: Represents extension subtags
- `PrivateUseTag`: Represents private use subtags
**Public API Classes:**

- `LanguageTag`: Main class for working with language tags (parse, build, normalize, validate)
- `LanguageTagBuilder`: Fluent builder for constructing language tags
- `LanguageLookup`: Language code conversion and matching (IETF ↔ ISO)
- `Iso6392Data`: ISO 639-2 language code data (`Create()`, `FromDataAsync()`, `FromJsonAsync()`)
- `Iso6393Data`: ISO 639-3 language code data (`Create()`, `FromDataAsync()`, `FromJsonAsync()`)
- `Rfc5646Data`: RFC 5646 / BCP 47 language subtag registry data (`Create()`, `FromDataAsync()`, `FromJsonAsync()`)
- `ExtensionTag`: Represents extension subtags (sealed record)
- `PrivateUseTag`: Represents private use subtags (sealed record)
- `LogOptions`: Static class for configuring library-wide logging via `ILoggerFactory`

**Internal Classes:**

- `LanguageTagParser`: Internal parser (use `LanguageTag.Parse()` instead)
- `LanguageTagParser`: Internal parser implementation (use `LanguageTag.Parse()` instead)

## Authoritative References

Expand Down
28 changes: 23 additions & 5 deletions CODESTYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,20 +168,38 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to
- C#, XML, YAML, JSON, Windows scripts: CRLF
- Linux scripts (`.sh`): LF

6. **`#region`**: Do not use. Prefer logical file/folder/namespace organization.
7. **Member ordering (StyleCop-like)**: Constants → fields → constructors → properties → indexers → methods → events → operators → finalizers → delegates → nested types
6. **`#region`**: Do not use regions. Prefer logical file/folder/namespace organization.
7. **Member ordering (StyleCop SA1201)**: conststatic readonly → static fields → instance readonly fields → instance fields → constructors → public (events → properties → indexers → methods → operators)non-public in same order → nested types

### Comments and Documentation

1. **XML documentation**
- `<GenerateDocumentationFile>true</GenerateDocumentationFile>`
- Missing XML comments for public APIs are suppressed (`.editorconfig`)
- Single-line summaries
- Must document all public surfaces.
- Single-line summaries, additional details in remarks, document input parameters, returns values, exceptions, and add crefs

```csharp
/// <summary>
/// This property always returns a value < 1.
/// Example of a single line summary.
/// </summary>
/// <remarks>
/// Additional important details about usage.
/// Multiple lines if needed.
/// </remarks>
/// <param name="category">
/// The quote category to request
/// </param>
/// <param name="cancellationToken">
/// A <see cref="System.Threading.CancellationToken"/> that can be used to cancel the request.
/// </param>
/// <returns>
/// A <see cref="string"/> containing the quote text.
/// </returns>
/// <exception cref="System.ArgumentException">
/// Thrown when <paramref name="category"/> is not a supported value.
/// </exception>
public async Task<string> GetQuoteOfTheDayAsync(string category, CancellationToken cancellationToken) {}
```

2. **Code analysis suppressions**
Expand Down Expand Up @@ -248,7 +266,7 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to

### Testing Conventions

1. **Framework**: xUnit with FluentAssertions
1. **Framework**: xUnit with AwesomeAssertions

```csharp
[Fact]
Expand Down
10 changes: 5 additions & 5 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ C# .NET library for ISO 639-2, ISO 639-3, RFC 5646 / BCP 47 language tags.
## Release History

- Version 1.2:
- Refactored the project to follow standard patterns across other projects.
- IO APIs are now async-only (`LoadDataAsync`, `LoadJsonAsync`, `SaveJsonAsync`, `GenCodeAsync`).
- Added logging support for `ILogger` or `ILoggerFactory` per class instance or statically.
- JSON load/save and codegen now stream directly to/from files, no intermediate text buffers.

- Refactored the project to follow standard patterns used across other projects.
- Added logging support configured through `LogOptions.SetFactory(ILoggerFactory)`.
- ⚠️ IO API's are async only, e.g. `LoadJson()` -> `async FromJsonAsync()`.
- ⚠️ Collection instantiation follows the `From` pattern, e.g. `LoadData()` -> `FromDataAsync()`.
- IO now streams directly to/from code/files without intermediate text buffers.
- Version 1.1:
- .NET 10 and AOT support.
- Refactored public surfaces to minimize internals exposure.
Expand Down
4 changes: 3 additions & 1 deletion LanguageTags.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
"ANTLR",
"arevela",
"boont",
"chamí",
"CLDR",
"codegen",
"csdevkit",
"datebadge",
"davidanson",
"derbend",
"dotnettools",
"Emberá",
"extlang",
"finalizers",
"gruntfuggly",
Expand Down Expand Up @@ -52,6 +54,7 @@
"pyfisch",
"Qaaa",
"Qabx",
"reparsed",
"rspeer",
"Serilog",
"Simoncic",
Expand Down Expand Up @@ -99,7 +102,6 @@
"github.vscode-github-actions",
"ms-azuretools.vscode-docker",
"ms-dotnettools.csdevkit",
"sanjulaganepola.github-local-actions",
"yzhang.markdown-all-in-one",
]
}
Expand Down
4 changes: 2 additions & 2 deletions LanguageTags/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal bool LogAndPropagate(
[CallerMemberName] string function = "unknown"
)
{
LogCatchException(logger, function, exception);
logger.LogCatchException(function, exception);
return false;
}

Expand All @@ -20,7 +20,7 @@ internal bool LogAndHandle(
[CallerMemberName] string function = "unknown"
)
{
LogCatchException(logger, function, exception);
logger.LogCatchException(function, exception);
return true;
}
}
Expand Down
Loading