2024-07-24 20:16:20 +09:00
#nullable enable
using System.Collections ;
using System.Collections.Frozen ;
using Dapper ;
using System.Collections.Generic ;
using System.Diagnostics.CodeAnalysis ;
using System.Linq ;
using System.Threading.Tasks ;
using BTCPayServer.Data ;
using Microsoft.EntityFrameworkCore ;
using System ;
using System.Security.Cryptography ;
using System.Text ;
using Newtonsoft.Json.Linq ;
using System.Data ;
using System.Data.Common ;
using Microsoft.Extensions.Logging ;
using static BTCPayServer . Services . LocalizerService ;
namespace BTCPayServer.Services
{
2024-10-18 16:06:51 +09:00
public interface IDefaultTranslationProvider
2024-10-14 14:11:00 +09:00
{
2024-10-18 16:06:51 +09:00
Task < KeyValuePair < string , string? > [ ] > GetDefaultTranslations ( ) ;
2024-10-14 14:11:00 +09:00
}
2024-10-18 16:06:51 +09:00
public class InMemoryDefaultTranslationProvider : IDefaultTranslationProvider
2024-10-14 14:11:00 +09:00
{
private readonly KeyValuePair < string , string? > [ ] _values ;
2024-10-18 16:06:51 +09:00
public InMemoryDefaultTranslationProvider ( KeyValuePair < string , string? > [ ] values )
2024-10-14 14:11:00 +09:00
{
_values = values ;
}
2024-10-18 16:06:51 +09:00
public Task < KeyValuePair < string , string? > [ ] > GetDefaultTranslations ( )
2024-10-14 14:11:00 +09:00
{
return Task . FromResult ( _values ) ;
}
}
2024-07-24 20:16:20 +09:00
public class LocalizerService
{
public LocalizerService (
ILogger < LocalizerService > logger ,
ApplicationDbContextFactory contextFactory ,
2024-10-14 14:11:00 +09:00
ISettingsAccessor < PoliciesSettings > settingsAccessor ,
2024-10-18 16:06:51 +09:00
IEnumerable < IDefaultTranslationProvider > defaultTranslationProviders )
2024-07-24 20:16:20 +09:00
{
_logger = logger ;
_ContextFactory = contextFactory ;
_settingsAccessor = settingsAccessor ;
2024-10-18 16:06:51 +09:00
_defaultTranslationProviders = defaultTranslationProviders ;
2024-07-24 20:16:20 +09:00
_LoadedTranslations = new LoadedTranslations ( Translations . Default , Translations . Default , Translations . DefaultLanguage ) ;
}
public record LoadedTranslations ( Translations Translations , Translations Fallback , string LangName ) ;
LoadedTranslations _LoadedTranslations ;
public Translations Translations = > _LoadedTranslations . Translations ;
private readonly ILogger < LocalizerService > _logger ;
private readonly ApplicationDbContextFactory _ContextFactory ;
private readonly ISettingsAccessor < PoliciesSettings > _settingsAccessor ;
2024-10-18 16:06:51 +09:00
private readonly IEnumerable < IDefaultTranslationProvider > _defaultTranslationProviders ;
2024-07-24 20:16:20 +09:00
/// <summary>
/// Load the translation of the server into memory
/// </summary>
/// <returns></returns>
public async Task Load ( )
{
try
{
_LoadedTranslations = await GetTranslations ( _settingsAccessor . Settings . LangDictionary ) ;
}
catch ( Exception ex )
{
_logger . LogWarning ( ex , "Failed to load translations" ) ;
throw ;
}
}
public async Task < LoadedTranslations > GetTranslations ( string dictionaryName )
{
var ctx = _ContextFactory . CreateContext ( ) ;
var conn = ctx . Database . GetDbConnection ( ) ;
var all = await conn . QueryAsync < ( bool fallback , string sentence , string? translation ) > (
"SELECT 'f'::BOOL fallback, sentence, translation FROM translations WHERE dict_id=@dict_id " +
"UNION ALL " +
"SELECT 't'::BOOL fallback, sentence, translation FROM translations WHERE dict_id=(SELECT fallback FROM lang_dictionaries WHERE dict_id=@dict_id)" ,
new
{
dict_id = dictionaryName ,
} ) ;
2024-10-14 14:11:00 +09:00
var defaultDict = Translations . Default ;
2024-10-18 16:06:51 +09:00
var loading = _defaultTranslationProviders . Select ( d = > d . GetDefaultTranslations ( ) ) . ToArray ( ) ;
2024-10-14 14:11:00 +09:00
Dictionary < string , string? > additionalDefault = new ( ) ;
foreach ( var defaultProvider in loading )
{
foreach ( var kv in await defaultProvider )
{
additionalDefault . TryAdd ( kv . Key , string . IsNullOrEmpty ( kv . Value ) ? kv . Key : kv . Value ) ;
}
}
defaultDict = new Translations ( additionalDefault , defaultDict ) ;
var fallback = new Translations ( all . Where ( a = > a . fallback ) . Select ( o = > KeyValuePair . Create ( o . sentence , o . translation ) ) , defaultDict ) ;
2024-07-24 20:16:20 +09:00
var translations = new Translations ( all . Where ( a = > ! a . fallback ) . Select ( o = > KeyValuePair . Create ( o . sentence , o . translation ) ) , fallback ) ;
return new LoadedTranslations ( translations , fallback , dictionaryName ) ;
}
public async Task Save ( Dictionary dictionary , Translations translations )
{
var loadedTranslations = await GetTranslations ( dictionary . DictionaryName ) ;
translations = translations . WithFallback ( loadedTranslations . Fallback ) ;
await using var ctx = _ContextFactory . CreateContext ( ) ;
var diffs = loadedTranslations . Translations . CalculateDiff ( translations ) ;
var conn = ctx . Database . GetDbConnection ( ) ;
List < string > keys = new List < string > ( ) ;
List < string > deletedKeys = new List < string > ( ) ;
List < string > values = new List < string > ( ) ;
// The basic idea here is that we can remove from
// the dictionary any translations which are the same
// as the fallback. This way, if the fallback get updated,
// it will also update the dictionary.
foreach ( var diff in diffs )
{
if ( diff is Translations . Diff . Added a )
{
if ( a . Value ! = loadedTranslations . Fallback [ a . Key ] )
{
keys . Add ( a . Key ) ;
values . Add ( a . Value ) ;
}
}
else if ( diff is Translations . Diff . Modified m )
{
if ( m . NewValue ! = loadedTranslations . Fallback [ m . Key ] )
{
keys . Add ( m . Key ) ;
values . Add ( m . NewValue ) ;
}
else
{
deletedKeys . Add ( m . Key ) ;
}
}
else if ( diff is Translations . Diff . Deleted d )
{
deletedKeys . Add ( d . Key ) ;
}
}
await conn . ExecuteAsync ( "INSERT INTO lang_translations SELECT @dict_id, sentence, translation FROM unnest(@keys, @values) AS t(sentence, translation) ON CONFLICT (dict_id, sentence) DO UPDATE SET translation = EXCLUDED.translation; " ,
new
{
dict_id = loadedTranslations . LangName ,
keys = keys . ToArray ( ) ,
values = values . ToArray ( )
} ) ;
await conn . ExecuteAsync ( "DELETE FROM lang_translations WHERE dict_id=@dict_id AND sentence=ANY(@keys)" ,
new
{
dict_id = loadedTranslations . LangName ,
keys = deletedKeys . ToArray ( )
} ) ;
if ( _LoadedTranslations . LangName = = loadedTranslations . LangName )
_LoadedTranslations = loadedTranslations with { Translations = translations } ;
}
public record Dictionary ( string DictionaryName , string? Fallback , string Source , JObject Metadata ) ;
public async Task < Dictionary [ ] > GetDictionaries ( )
{
await using var ctx = _ContextFactory . CreateContext ( ) ;
var db = ctx . Database . GetDbConnection ( ) ;
var rows = await db . QueryAsync < ( string dict_id , string? fallback , string? source , string? metadata ) > ( "SELECT * FROM lang_dictionaries" ) ;
return rows . Select ( r = > new Dictionary ( r . dict_id , r . fallback , r . source ? ? "" , JObject . Parse ( r . metadata ? ? "{}" ) ) ) . ToArray ( ) ;
}
public async Task < Dictionary ? > GetDictionary ( string name )
{
await using var ctx = _ContextFactory . CreateContext ( ) ;
var db = ctx . Database . GetDbConnection ( ) ;
var r = await db . QueryFirstAsync ( "SELECT * FROM lang_dictionaries WHERE dict_id=@dict_id" , new { dict_id = name } ) ;
if ( r is null )
return null ;
return new Dictionary ( r . dict_id , r . fallback , r . source ? ? "" , JObject . Parse ( r . metadata ? ? "{}" ) ) ;
}
public async Task < Dictionary > CreateDictionary ( string langName , string? fallback , string source )
{
await using var ctx = _ContextFactory . CreateContext ( ) ;
var db = ctx . Database . GetDbConnection ( ) ;
await db . ExecuteAsync ( "INSERT INTO lang_dictionaries (dict_id, fallback, source) VALUES (@langName, @fallback, @source)" , new { langName , fallback , source } ) ;
return new Dictionary ( langName , fallback , source ? ? "" , new JObject ( ) ) ;
}
public async Task DeleteDictionary ( string dictionary )
{
await using var ctx = _ContextFactory . CreateContext ( ) ;
var db = ctx . Database . GetDbConnection ( ) ;
await db . ExecuteAsync ( "DELETE FROM lang_dictionaries WHERE dict_id=@dict_id AND source='Custom'" , new { dict_id = dictionary } ) ;
}
}
}