2026-10-15 01:18:13 +02:00
using System.Text.Encodings.Web ;
using System.Text.Json ;
using System.Text.Json.Nodes ;
using System.Text.RegularExpressions ;
var root = FindRoot ();
var folder = Path . Combine ( root , "src" , "MeWeb.App" , "Strings" );
var options = new JsonSerializerOptions { WriteIndented = true , Encoder = JavaScriptEncoder . UnsafeRelaxedJsonEscaping };
var placeholder = new Regex ( @"\{\d+\}" );
if ( args . Length >= 1 && args [ 0 ] == "check" ) return Check ();
if ( args . Length == 3 && args [ 0 ] == "set" ) return Set ( args [ 1 ], args [ 2 ]);
2026-10-15 04:39:03 +02:00
if ( args . Length == 2 && args [ 0 ] == "merge" ) return Merge ( args [ 1 ]);
if ( args . Length == 3 && args [ 0 ] == "import" ) return Import ( args [ 1 ], args [ 2 ]);
if ( args . Length == 2 && args [ 0 ] == "drop" ) return Drop ( args [ 1 ]);
Console . Error . WriteLine ( "usage: dotnet run tools/strings.cs -- check | set <Key> <values.json> | merge <keys.json> | import <code> <strings.json> | drop <Key>" );
Console . Error . WriteLine ( " values.json maps a language code to its text; keys.json maps keys to such maps; strings.json maps keys to one language's text" );
2026-10-15 01:18:13 +02:00
return 2 ;
int Check ()
{
var english = Read ( "en" );
var problems = 0 ;
foreach ( var file in Directory . GetFiles ( folder , "*.json" ). OrderBy ( f => f , StringComparer . Ordinal ))
{
var code = Path . GetFileNameWithoutExtension ( file );
if ( code == "en" ) continue ;
var values = Read ( code );
var missing = english . Keys . Where ( k => ! values . ContainsKey ( k )). ToList ();
var extra = values . Keys . Where ( k => ! english . ContainsKey ( k )). ToList ();
var mismatched = english . Keys . Where ( values . ContainsKey )
. Where ( k => ! placeholder . Matches ( english [ k ]). Select ( m => m . Value ). Order (). SequenceEqual ( placeholder . Matches ( values [ k ]). Select ( m => m . Value ). Order ())). ToList ();
if ( missing . Count + extra . Count + mismatched . Count == 0 ) continue ;
problems ++;
Console . WriteLine ( $"{code}: missing [{string.Join(" , ", missing)}] extra [{string.Join(" , ", extra)}] placeholders [{string.Join(" , ", mismatched)}]" );
}
Console . WriteLine ( problems == 0 ? $"strings: OK ({english.Count} keys)" : $"strings: {problems} language(s) to fix" );
return problems == 0 ? 0 : 1 ;
}
int Set ( string key , string valuesPath )
{
var values = JsonNode . Parse ( File . ReadAllText ( valuesPath )). AsObject ();
if ( values [ "en" ] is null ) throw new InvalidOperationException ( "values.json must carry at least \"en\"" );
foreach ( var ( code , value ) in values )
{
var strings = Read ( code );
strings [ key ] = ( string ) value ;
Write ( code , strings );
}
Console . WriteLine ( $"{key}: set in {values.Count} language(s)" );
return 0 ;
}
2026-10-15 04:39:03 +02:00
int Merge ( string path )
{
var keys = JsonNode . Parse ( File . ReadAllText ( path )). AsObject ();
var byLanguage = new Dictionary < string , SortedDictionary < string , string >>();
foreach ( var ( key , values ) in keys )
{
if ( values ?[ "en" ] is null ) throw new InvalidOperationException ( $"{key} carries no \" en \ "" );
foreach ( var ( code , value ) in values . AsObject ())
{
if (! byLanguage . TryGetValue ( code , out var strings )) byLanguage [ code ] = strings = Read ( code );
strings [ key ] = ( string ) value ;
}
}
foreach ( var ( code , strings ) in byLanguage ) Write ( code , strings );
Console . WriteLine ( $"merge: {keys.Count} key(s) into {byLanguage.Count} language(s)" );
return 0 ;
}
int Import ( string code , string path )
{
var english = Read ( "en" );
var incoming = JsonNode . Parse ( File . ReadAllText ( path )). AsObject ();
var strings = Read ( code );
var unknown = new List < string >();
foreach ( var ( key , value ) in incoming )
{
if (! english . ContainsKey ( key ))
{
unknown . Add ( key );
continue ;
}
strings [ key ] = ( string ) value ;
}
Write ( code , strings );
Console . WriteLine ( $"import {code}: {incoming.Count - unknown.Count} key(s){(unknown.Count > 0 ? $" , ignored unknown [{ string . Join ( ", " , unknown )}] " : "")}" );
return unknown . Count == 0 ? 0 : 1 ;
}
int Drop ( string key )
{
var count = 0 ;
foreach ( var file in Directory . GetFiles ( folder , "*.json" ))
{
var code = Path . GetFileNameWithoutExtension ( file );
var strings = Read ( code );
if (! strings . Remove ( key )) continue ;
Write ( code , strings );
count ++;
}
Console . WriteLine ( $"{key}: dropped from {count} language(s)" );
return 0 ;
}
2026-10-15 01:18:13 +02:00
SortedDictionary < string , string > Read ( string code )
{
var path = Path . Combine ( folder , $"{code}.json" );
if (! File . Exists ( path )) return new SortedDictionary < string , string >( StringComparer . Ordinal );
var node = JsonNode . Parse ( File . ReadAllText ( path )). AsObject ();
return new SortedDictionary < string , string >( node . ToDictionary ( p => p . Key , p => ( string ) p . Value ), StringComparer . Ordinal );
}
void Write ( string code , SortedDictionary < string , string > strings )
{
var node = new JsonObject ();
foreach ( var ( key , value ) in strings ) node [ key ] = value ;
File . WriteAllText ( Path . Combine ( folder , $"{code}.json" ), node . ToJsonString ( options ) + "\n" );
}
static string FindRoot ()
{
var dir = new DirectoryInfo ( Directory . GetCurrentDirectory ());
while ( dir is not null && ! File . Exists ( Path . Combine ( dir . FullName , "MeWeb.slnx" ))) dir = dir . Parent ;
return dir ?. FullName ?? throw new InvalidOperationException ( "run from inside the repository (MeWeb.slnx not found)" );
}