mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-05-09 08:04:13 -04:00
Merge pull request #1725 from Jo-Be-Co/language_and_region
Extend <language> and <locale> tags
This commit is contained in:
@@ -256,7 +256,7 @@ public class Book
|
||||
IsAbridged |= isAbridged;
|
||||
IsSpatial = isSpatial ?? IsSpatial;
|
||||
DatePublished = datePublished ?? DatePublished;
|
||||
Language = language?.FirstCharToUpper() ?? Language;
|
||||
Language = language?.Trim().FirstCharToUpper() ?? Language;
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{AudibleProductId}] {TitleWithSubtitle}";
|
||||
|
||||
@@ -73,7 +73,7 @@ public class MockLibraryBook : LibraryBook
|
||||
|
||||
public static MockLibraryBook CreateBook(
|
||||
string account = "someone@email.co",
|
||||
bool absetFromLastScan = false,
|
||||
bool absentFromLastScan = false,
|
||||
DateTime? dateAdded = null,
|
||||
DateTime? datePublished = null,
|
||||
DateTime? includedUntil = null,
|
||||
@@ -120,7 +120,7 @@ public class MockLibraryBook : LibraryBook
|
||||
includedUntil,
|
||||
isAudiblePlus)
|
||||
{
|
||||
AbsentFromLastScan = absetFromLastScan
|
||||
AbsentFromLastScan = absentFromLastScan
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ public static class UtilityExtensions
|
||||
Title = libraryBook.Book.Title,
|
||||
Subtitle = libraryBook.Book.Subtitle,
|
||||
TitleWithSubtitle = libraryBook.Book.TitleWithSubtitle,
|
||||
Locale = libraryBook.Book.Locale,
|
||||
Locale = new LocaleDto(libraryBook.Book.Locale),
|
||||
YearPublished = libraryBook.Book.DatePublished?.Year,
|
||||
DatePublished = libraryBook.Book.DatePublished,
|
||||
|
||||
@@ -72,7 +72,7 @@ public static class UtilityExtensions
|
||||
IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(),
|
||||
|
||||
LengthInMinutes = TimeSpan.FromMinutes(libraryBook.Book.LengthInMinutes),
|
||||
Language = libraryBook.Book.Language?.Trim(),
|
||||
Language = libraryBook.Book.Language is null ? null : new CultureInfoDto(libraryBook.Book.Language),
|
||||
Codec = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.CodecString,
|
||||
BitRate = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.BitRate,
|
||||
SampleRate = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.SampleRate,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
|
||||
namespace FileManager.NamingTemplate;
|
||||
|
||||
@@ -32,6 +33,9 @@ public static partial class CommonFormatters
|
||||
public static string StringFormatter(ITemplateTag _, string? value, string? formatString, CultureInfo? culture)
|
||||
=> _StringFormatter(value, formatString, culture);
|
||||
|
||||
public static string _StringFormatter(string? value, string? formatString, IFormatProvider? provider)
|
||||
=> _StringFormatter(value, formatString, GetCultureInfo(provider));
|
||||
|
||||
private static string _StringFormatter(string? value, string? formatString, CultureInfo? culture)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return string.Empty;
|
||||
@@ -61,24 +65,36 @@ public static partial class CommonFormatters
|
||||
if (string.IsNullOrWhiteSpace(templateString)) return "";
|
||||
|
||||
// is this function is called from toString implementation of the IFormattable interface, we only get a IFormatProvider
|
||||
var culture = provider as CultureInfo ?? provider?.GetFormat(typeof(CultureInfo)) as CultureInfo;
|
||||
return CollapseSpacesAndTrimRegex().Replace(TagFormatRegex().Replace(templateString, GetValueForMatchingTag), "");
|
||||
var culture = GetCultureInfo(provider);
|
||||
var oldUiCulture = Thread.CurrentThread.CurrentUICulture;
|
||||
var result = CollapseSpacesAndTrimRegex().Replace(TagFormatRegex().Replace(templateString, GetValueForMatchingTag), "");
|
||||
Thread.CurrentThread.CurrentUICulture = oldUiCulture;
|
||||
return result;
|
||||
|
||||
string GetValueForMatchingTag(Match m)
|
||||
{
|
||||
var tag = m.Groups["tag"].Value;
|
||||
if (!replacements.TryGetValue(tag, out var getter)) return m.Value;
|
||||
|
||||
var lang = m.Groups["lang"].ValueOrNull();
|
||||
var cultureToUse = lang is null ? culture : CultureInfo.GetCultureInfo(lang);
|
||||
Thread.CurrentThread.CurrentUICulture = cultureToUse ?? oldUiCulture;
|
||||
|
||||
var value = getter(toFormat);
|
||||
var format = m.Groups["format"].ValueOrNull();
|
||||
return value switch
|
||||
{
|
||||
IFormattable formattable => formattable.ToString(format, provider),
|
||||
_ => _StringFormatter(value?.ToString(), format, culture),
|
||||
IFormattable formattable => formattable.ToString(format, cultureToUse),
|
||||
_ => _StringFormatter(value?.ToString(), format, cultureToUse),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static CultureInfo? GetCultureInfo(IFormatProvider? provider)
|
||||
{
|
||||
return provider as CultureInfo ?? provider?.GetFormat(typeof(CultureInfo)) as CultureInfo;
|
||||
}
|
||||
|
||||
// Matches runs of spaces followed by a space as well as runs of spaces at the beginning or the end of a string (does NOT touch tabs/newlines).
|
||||
[GeneratedRegex(@"^ +| +(?=$| )")]
|
||||
private static partial Regex CollapseSpacesAndTrimRegex();
|
||||
@@ -87,8 +103,8 @@ public static partial class CommonFormatters
|
||||
// The tagname may be followed by an optional format specifier separated by a colon.
|
||||
// All other parts of the template string are left untouched as well as the braces where the tagname is unknown.
|
||||
// TemplateStringFormatter will use a dictionary to lookup the tagname and the corresponding value getter.
|
||||
[GeneratedRegex("""\{(?<tag>[A-Z]+|#)(?::(?<format>(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?))?\}""", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex TagFormatRegex();
|
||||
[GeneratedRegex("""\{(?<tag>[A-Z0-9]+|#)(?:@(?<lang>[a-z-]+))?(?::(?<format>(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?))?\}""", RegexOptions.IgnoreCase)]
|
||||
public static partial Regex TagFormatRegex();
|
||||
|
||||
public static string FormattableFormatter(ITemplateTag _, IFormattable? value, string? formatString, CultureInfo? culture)
|
||||
=> value?.ToString(formatString, culture) ?? "";
|
||||
@@ -100,12 +116,10 @@ public static partial class CommonFormatters
|
||||
{
|
||||
culture ??= CultureInfo.CurrentCulture;
|
||||
if (!int.TryParse(formatString, out var numDigits) || numDigits <= 0) return value.ToString(formatString, culture);
|
||||
//Zero-pad the integer part
|
||||
var strValue = value.ToString(culture);
|
||||
var decIndex = culture.CompareInfo.IndexOf(strValue, culture.NumberFormat.NumberDecimalSeparator);
|
||||
var zeroPad = decIndex == -1 ? int.Max(0, numDigits - strValue.Length) : int.Max(0, numDigits - decIndex);
|
||||
|
||||
return new string('0', zeroPad) + strValue;
|
||||
//Zero-pad the integer part
|
||||
formatString = new string('0', numDigits) + ".################";
|
||||
return value.ToString(formatString, culture);
|
||||
}
|
||||
|
||||
public static string MinutesFormatter(ITemplateTag templateTag, TimeSpan value, string? formatString, CultureInfo? culture)
|
||||
@@ -163,7 +177,7 @@ public static partial class CommonFormatters
|
||||
[GeneratedRegex("""
|
||||
(?x) # option x: ignore all unescaped whitespace in pattern and allow comments starting with #
|
||||
(?<=\G(?: # We lookbehind up to the start or the end of the last match for a number format.
|
||||
\\. # - '\' escapes allways the next character. Especially further '\' and the closing ']'
|
||||
\\. # - '\' escapes always the next character. Especially further '\' and the closing ']'
|
||||
| '(?:[^']|'')*' # - allow 'string' to be included in the format, with '' being an escaped ' character
|
||||
| "(?:[^"]|"")*" # - allow "string" to be included in the format, with "" being an escaped " character
|
||||
| . # - match any character. This will not catch the number format at first. Because ...
|
||||
@@ -172,7 +186,7 @@ public static partial class CommonFormatters
|
||||
(?:\#[\#,.]*)? # - For grouping a number format may start with `#` and grouping hints `,` or even a decimal point `.`.
|
||||
D # - At least one unescaped, unquoted uppercase D must be included in the format to indicate that this is a total days format.
|
||||
(?:(?: # - Before further D's, there may be any combination of escaped characters and quoted strings.
|
||||
\\. # - '\' escapes allways the next character. Especially further '\' and the closing ']'
|
||||
\\. # - '\' escapes always the next character. Especially further '\' and the closing ']'
|
||||
| '(?:[^']|'')*' # - allow 'string' to be included in the format, with '' being an escaped ' character
|
||||
| "(?:[^"]|"")*" # - allow "string" to be included in the format, with "" being an escaped " character
|
||||
)* [\#,.%‰D]+ # After escaped characters and quoted strings, there needs to be at least one more real number format character (which may be D as well).
|
||||
|
||||
@@ -116,7 +116,7 @@ public partial class ConditionalTagCollection<TClass>(bool caseSensitive = true)
|
||||
(?<!\s)) # - don't let <property> end with a whitepace. Otherwise "<tagname [foobar]->" would be matchable.
|
||||
(?:\s*\[\s* # optional check details enclosed in '[' and ']'. Check shall start with an operator. So match whitespace first
|
||||
(?<check> # - capture inner part as <check>
|
||||
(?:\\. # - '\' escapes allways the next character. Especially further '\' and the closing ']'
|
||||
(?:\\. # - '\' escapes always the next character. Especially further '\' and the closing ']'
|
||||
|[^\\\]])* ) # - match any character except '\' and ']'. Check may end in whitespace!
|
||||
\])? # - closing the check part
|
||||
)? # end of optional property and check part
|
||||
|
||||
@@ -194,7 +194,7 @@ public class PropertyTagCollection<TClass> : TagCollection
|
||||
{TagNameForRegex()} # next the tagname needs to be matched with space being made optional. Also escape all '#'
|
||||
(?:\s* # optional whitespace
|
||||
\[ (?<format> # optional format details enclosed in '[' and ']'. Capture inner part as <format>.
|
||||
(?:\\. # - '\' escapes allways the next character. Especially further '\' and the closing ']'
|
||||
(?:\\. # - '\' escapes always the next character. Especially further '\' and the closing ']'
|
||||
|'(?:[^']|'')*' # - allow 'string' to be included in the format, with '' being an escaped ' character
|
||||
|"(?:[^"]|"")*" # - allow "string" to be included in the format, with "" being an escaped " character
|
||||
|[^\\\]])* ) # - match any character except '\' and ']'. Format may end in whitespace!
|
||||
|
||||
@@ -53,7 +53,7 @@ public partial class ThemePreviewControl : UserControl
|
||||
{
|
||||
yield return MockLibraryBook.CreateBook(title: "Some Book 1", subtitle: "The Theming", dateAdded: System.DateTime.Now.AddDays(4)).WithBookStatus(LiberatedStatus.Liberated);
|
||||
yield return MockLibraryBook.CreateBook(title: "Some Book 2", dateAdded: System.DateTime.Now.AddDays(3)).WithBookStatus(LiberatedStatus.PartialDownload);
|
||||
yield return MockLibraryBook.CreateBook(title: "Some Book 3", dateAdded: System.DateTime.Now.AddDays(2), absetFromLastScan: true).WithPdfStatus(LiberatedStatus.NotLiberated);
|
||||
yield return MockLibraryBook.CreateBook(title: "Some Book 3", dateAdded: System.DateTime.Now.AddDays(2), absentFromLastScan: true).WithPdfStatus(LiberatedStatus.NotLiberated);
|
||||
yield return MockLibraryBook.CreateBook(title: "Some Book 4", dateAdded: System.DateTime.Now.AddDays(1)).WithBookStatus(LiberatedStatus.Error);
|
||||
yield return MockLibraryBook.CreateBook(title: "Some Series", subtitle: "", contentType: ContentType.Parent).AddSeries("Some Series", 0);
|
||||
yield return MockLibraryBook.CreateBook(title: "Some Episode", subtitle: "Episode 1", contentType: ContentType.Episode).AddSeries("Some Series", 1);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="10.1.4.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.2" />
|
||||
<PackageReference Include="NameParserSharp" Version="1.5.0" />
|
||||
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
|
||||
|
||||
86
Source/LibationFileManager/Templates/CultureInfoDto.cs
Normal file
86
Source/LibationFileManager/Templates/CultureInfoDto.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using FileManager.NamingTemplate;
|
||||
|
||||
namespace LibationFileManager.Templates;
|
||||
|
||||
public record CultureInfoDto : IFormattable
|
||||
{
|
||||
private CultureInfo? Value { get; }
|
||||
private string DefaultFormat { get; }
|
||||
public string Original { get; }
|
||||
|
||||
public static CultureInfoDto OfCurrentUi()
|
||||
{
|
||||
return new CultureInfoDto(CultureInfo.DefaultThreadCurrentUICulture ?? CultureInfo.CurrentUICulture, CultureInfo.CurrentUICulture.Name, "{N}");
|
||||
}
|
||||
|
||||
public static CultureInfoDto OfCurrentOs()
|
||||
{
|
||||
return new CultureInfoDto(CultureInfo.DefaultThreadCurrentCulture ?? CultureInfo.CurrentCulture, CultureInfo.CurrentCulture.Name, "{N}");
|
||||
}
|
||||
|
||||
public CultureInfoDto(string hint) : this(hint, "{O}")
|
||||
{
|
||||
}
|
||||
|
||||
public CultureInfoDto(string hint, string defaultFormat) : this(GetCulture(hint), hint, defaultFormat)
|
||||
{
|
||||
}
|
||||
|
||||
public CultureInfoDto(CultureInfo? value, string hint, string defaultFormat)
|
||||
{
|
||||
Original = hint;
|
||||
DefaultFormat = defaultFormat;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
private static CultureInfo? GetCulture(string input)
|
||||
{
|
||||
return
|
||||
GetCulture(input, CultureTypes.NeutralCultures) ??
|
||||
GetCulture(input, CultureTypes.SpecificCultures);
|
||||
}
|
||||
|
||||
private static CultureInfo? GetCulture(string input, CultureTypes types)
|
||||
{
|
||||
var cultures = CultureInfo.GetCultures(types);
|
||||
return Match(cultures, input, c => c.Name) ??
|
||||
Match(cultures, input, c => c.TwoLetterISOLanguageName) ??
|
||||
Match(cultures, input, c => c.ThreeLetterISOLanguageName) ??
|
||||
Match(cultures, input, c => c.EnglishName);
|
||||
}
|
||||
|
||||
private static CultureInfo? Match(IEnumerable<CultureInfo> cultures, string input, Func<CultureInfo, string?> selector, StringComparison cmp = StringComparison.OrdinalIgnoreCase)
|
||||
{
|
||||
return cultures.FirstOrDefault(c => string.Equals(selector(c), input, cmp));
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, Func<CultureInfoDto, object?>> FormatReplacements = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "ID", dto => dto.Value?.Name },
|
||||
{ "I", dto => dto.Value?.TwoLetterISOLanguageName },
|
||||
{ "I2", dto => dto.Value?.TwoLetterISOLanguageName },
|
||||
{ "I3", dto => dto.Value?.ThreeLetterISOLanguageName },
|
||||
{ "W", dto => dto.Value?.ThreeLetterWindowsLanguageName },
|
||||
{ "E", dto => dto.Value?.EnglishName },
|
||||
{ "N", dto => dto.Value?.NativeName },
|
||||
{ "O", dto => dto.Original },
|
||||
{ "D", dto => dto.Value?.DisplayName }, // localized
|
||||
};
|
||||
|
||||
public override string ToString() => ToString(DefaultFormat, CultureInfo.CurrentCulture);
|
||||
|
||||
public string ToString(string? format, IFormatProvider? provider)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(format)) format = DefaultFormat;
|
||||
return format switch
|
||||
{
|
||||
_ when CommonFormatters.TagFormatRegex().IsMatch(format) => CommonFormatters.TemplateStringFormatter(this, format, provider, FormatReplacements),
|
||||
_ when format == DefaultFormat => CommonFormatters._StringFormatter(Original, format, provider),
|
||||
_ => CommonFormatters._StringFormatter(CommonFormatters.TemplateStringFormatter(this, DefaultFormat, provider, FormatReplacements), format, provider)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ public class BookDto
|
||||
public string? Title { get; set; }
|
||||
public string? Subtitle { get; set; }
|
||||
public string? TitleWithSubtitle { get; set; }
|
||||
public string? Locale { get; set; }
|
||||
public LocaleDto? Locale { get; set; }
|
||||
public int? YearPublished { get; set; }
|
||||
|
||||
public IEnumerable<ContributorDto>? Authors { get; set; }
|
||||
@@ -34,7 +34,7 @@ public class BookDto
|
||||
public string? Codec { get; set; }
|
||||
public DateTime FileDate { get; set; } = DateTime.Now;
|
||||
public DateTime? DatePublished { get; set; }
|
||||
public string? Language { get; set; }
|
||||
public CultureInfoDto? Language { get; set; }
|
||||
public string? LibationVersion { get; set; }
|
||||
public string? FileVersion { get; set; }
|
||||
}
|
||||
|
||||
125
Source/LibationFileManager/Templates/LocaleDto.cs
Normal file
125
Source/LibationFileManager/Templates/LocaleDto.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using AudibleApi;
|
||||
using FileManager.NamingTemplate;
|
||||
|
||||
namespace LibationFileManager.Templates;
|
||||
|
||||
public partial record LocaleDto : IFormattable
|
||||
{
|
||||
public string Original { get; }
|
||||
public Locale Locale { get; set; }
|
||||
private RegionInfo? Value { get; }
|
||||
private CultureInfo? Culture { get; }
|
||||
private string DefaultFormat { get; }
|
||||
|
||||
|
||||
public LocaleDto(string hint) : this(hint, "{O}")
|
||||
{
|
||||
}
|
||||
|
||||
public LocaleDto(string hint, string defaultFormat) : this(Localization.Get(hint), hint, defaultFormat)
|
||||
{
|
||||
}
|
||||
|
||||
public LocaleDto(Locale locale, string hint, string defaultFormat)
|
||||
{
|
||||
var (regionInfo, cultureInfo) = GetRegion(locale.Language, locale.CountryCode, hint);
|
||||
|
||||
Original = hint;
|
||||
Locale = locale;
|
||||
Value = regionInfo;
|
||||
Culture = cultureInfo ?? (regionInfo is null ? null : GetCultureInfo(regionInfo));
|
||||
DefaultFormat = defaultFormat;
|
||||
}
|
||||
|
||||
private static (RegionInfo?, CultureInfo?) GetRegion(string language, string countrcode, string input)
|
||||
{
|
||||
CultureInfo? culture = null;
|
||||
if (language != string.Empty)
|
||||
try
|
||||
{
|
||||
culture = CultureInfo.GetCultureInfo(language.Length == 2 ? $"{language}-{countrcode}" : language);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return (new RegionInfo(countrcode), culture);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
if (culture is not null)
|
||||
return (new RegionInfo(culture.Name), culture);
|
||||
|
||||
try
|
||||
{
|
||||
return (new RegionInfo(input), culture);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return CultureInfo.GetCultures(CultureTypes.SpecificCultures)
|
||||
.Select(c => (new RegionInfo(c.Name), c))
|
||||
.FirstOrDefault(rAndC =>
|
||||
string.Equals(rAndC.Item1.EnglishName, input, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetLocalizedRegionName()
|
||||
{
|
||||
return Culture is null ? null : GetCountryName(Culture.DisplayName) ?? GetCountryName(Culture.EnglishName);
|
||||
}
|
||||
|
||||
private static string? GetCountryName(string localized)
|
||||
{
|
||||
return ExtractRegionName().Match(localized).Groups["displayName"].ValueOrNull();
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\((?<displayName>.+)\)")]
|
||||
private static partial Regex ExtractRegionName();
|
||||
|
||||
private static CultureInfo? GetCultureInfo(RegionInfo region)
|
||||
{
|
||||
// find culture for region
|
||||
return CultureInfo.GetCultures(CultureTypes.SpecificCultures)
|
||||
.FirstOrDefault(c => Equals(new RegionInfo(c.Name), region));
|
||||
}
|
||||
|
||||
|
||||
private static readonly Dictionary<string, Func<LocaleDto, object?>> FormatReplacements = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "ID", dto => dto.Locale?.MarketPlaceId },
|
||||
{ "I", dto => dto.Value?.TwoLetterISORegionName },
|
||||
{ "I2", dto => dto.Value?.TwoLetterISORegionName },
|
||||
{ "I3", dto => dto.Value?.ThreeLetterISORegionName },
|
||||
{ "W", dto => dto.Value?.ThreeLetterWindowsRegionName },
|
||||
{ "E", dto => dto.Value?.EnglishName },
|
||||
{ "N", dto => dto.Value?.NativeName },
|
||||
{ "O", dto => dto.Original },
|
||||
{ "T", dto => dto.Locale.TopDomain },
|
||||
{ "L", dto => dto.Culture?.Name },
|
||||
{ "D", dto => dto.GetLocalizedRegionName() }, // localized
|
||||
};
|
||||
|
||||
public override string ToString() => ToString(DefaultFormat, CultureInfo.CurrentCulture);
|
||||
|
||||
public string ToString(string? format, IFormatProvider? provider)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(format)) format = DefaultFormat;
|
||||
return format switch
|
||||
{
|
||||
_ when CommonFormatters.TagFormatRegex().IsMatch(format) => CommonFormatters.TemplateStringFormatter(this, format, provider, FormatReplacements),
|
||||
_ when format == DefaultFormat => CommonFormatters._StringFormatter(Original, format, provider),
|
||||
_ => CommonFormatters._StringFormatter(CommonFormatters.TemplateStringFormatter(this, DefaultFormat, provider, FormatReplacements), format, provider)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ public class TemplateEditor<T> : ITemplateEditor where T : Templates, ITemplate,
|
||||
Title = "A Study in Scarlet",
|
||||
TitleWithSubtitle = "A Study in Scarlet: A Sherlock Holmes Novel",
|
||||
Subtitle = "A Sherlock Holmes Novel",
|
||||
Locale = "us",
|
||||
Locale = new LocaleDto("us"),
|
||||
YearPublished = 2017,
|
||||
Authors = [new("Arthur Conan Doyle", "B000AQ43GQ"), new("Stephen Fry - introductions", "B000APAGVS")],
|
||||
Narrators = [new("Stephen Fry", null)],
|
||||
@@ -74,7 +74,7 @@ public class TemplateEditor<T> : ITemplateEditor where T : Templates, ITemplate,
|
||||
BitRate = 128,
|
||||
SampleRate = 44100,
|
||||
Channels = 2,
|
||||
Language = "English"
|
||||
Language = new CultureInfoDto("English"),
|
||||
};
|
||||
|
||||
private static readonly MultiConvertFileProperties DefaultMultipartProperties
|
||||
|
||||
@@ -49,6 +49,8 @@ public sealed class TemplateTags : ITemplateTag
|
||||
public static TemplateTags YearPublished { get; } = new("year", "Year published");
|
||||
public static TemplateTags Language { get; } = new("language", "Book's language");
|
||||
public static TemplateTags LanguageShort { get; } = new("language short", "Book's language abbreviated. Eg: ENG");
|
||||
public static TemplateTags UI { get; } = new("ui", "UI language");
|
||||
public static TemplateTags OS { get; } = new("os", "OS language");
|
||||
|
||||
public static TemplateTags FileDate { get; } = new("file date", "File date/time. e.g. yyyy-MM-dd HH-mm", $"<file date [{CommonFormatters.DefaultDateFormat}]>", "<file date [...]>");
|
||||
public static TemplateTags DatePublished { get; } = new("pub date", "Publication date. e.g. yyyy-MM-dd", $"<pub date [{CommonFormatters.DefaultDateFormat}]>", "<pub date [...]>");
|
||||
|
||||
@@ -278,12 +278,14 @@ public abstract class Templates
|
||||
{ TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter, SeriesListFormat.Finalizer },
|
||||
{ TemplateTags.FirstSeries, lb => lb.FirstSeries, CommonFormatters.FormattableFormatter },
|
||||
{ TemplateTags.SeriesNumber, lb => lb.FirstSeries?.Order, CommonFormatters.FormattableFormatter },
|
||||
{ TemplateTags.Language, lb => lb.Language },
|
||||
{ TemplateTags.Language, lb => lb.Language, CommonFormatters.FormattableFormatter },
|
||||
//Don't allow formatting of LanguageShort
|
||||
{ TemplateTags.LanguageShort, lb => lb.Language, CommonFormatters.LanguageShortFormatter },
|
||||
{ TemplateTags.LanguageShort, lb => lb.Language?.Original, CommonFormatters.LanguageShortFormatter },
|
||||
{ TemplateTags.UI, _ => CultureInfoDto.OfCurrentUi(), CommonFormatters.FormattableFormatter },
|
||||
{ TemplateTags.OS, _ => CultureInfoDto.OfCurrentOs(), CommonFormatters.FormattableFormatter },
|
||||
{ TemplateTags.Account, lb => lb.Account },
|
||||
{ TemplateTags.AccountNickname, lb => lb.AccountNickname },
|
||||
{ TemplateTags.Locale, lb => lb.Locale },
|
||||
{ TemplateTags.Locale, lb => lb.Locale, CommonFormatters.FormattableFormatter },
|
||||
{ TemplateTags.YearPublished, lb => lb.YearPublished },
|
||||
{ TemplateTags.DatePublished, lb => lb.DatePublished },
|
||||
{ TemplateTags.DateAdded, lb => lb.DateAdded },
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using static TemplatesTests.Shared;
|
||||
|
||||
[assembly: Parallelize]
|
||||
@@ -43,7 +44,7 @@ namespace TemplatesTests
|
||||
FileDate = new DateTime(2023, 1, 28, 0, 0, 0),
|
||||
AudibleProductId = "asin",
|
||||
Title = "A Study in Scarlet: A Sherlock Holmes Novel",
|
||||
Locale = "us",
|
||||
Locale = new LocaleDto("us"),
|
||||
YearPublished = null, // explicitly null
|
||||
Authors = [new("Arthur Conan Doyle", "B000AQ43GQ"), new("Stephen Fry - introductions", "B000APAGVS")],
|
||||
Narrators = [], // explicitly empty list
|
||||
@@ -51,7 +52,7 @@ namespace TemplatesTests
|
||||
BitRate = 128,
|
||||
SampleRate = 44100,
|
||||
Channels = 2,
|
||||
Language = "English",
|
||||
Language = new CultureInfoDto("English"),
|
||||
Subtitle = "An Audible Original Drama",
|
||||
TitleWithSubtitle = "A Study in Scarlet: An Audible Original Drama",
|
||||
Codec = @"AAC[LC]\MP3", // special chars added
|
||||
@@ -761,6 +762,131 @@ namespace TemplatesTests
|
||||
.GetName(bookDto, new MultiConvertFileProperties { OutputFileName = string.Empty })
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("English", "<language>", "English")]
|
||||
[DataRow("English", "<language[4u]>", "ENGL")]
|
||||
[DataRow("English", "<language short>", "ENG")]
|
||||
[DataRow("English", "<language short[1l]>", "ENG")]
|
||||
[DataRow("English", "<language[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, N:{N}, O:{O}]>", "ID:en, 2:en, 3:eng, W:ENU, D:inglés, E:English, N:English, O:English")]
|
||||
[DataRow("en", "<language[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, N:{N}, O:{O}]>", "ID:en, 2:en, 3:eng, W:ENU, D:inglés, E:English, N:English, O:en")]
|
||||
[DataRow("fr", "<language[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, N:{N}, O:{O}]>", "ID:fr, 2:fr, 3:fra, W:FRA, D:francés, E:French, N:français, O:fr")]
|
||||
[DataRow("fr-ca", "<language[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, N:{N}, O:{O}]>",
|
||||
"ID:fr-CA, 2:fr, 3:fra, W:FRC, D:francés (Canadá), E:French (Canada), N:français (Canada), O:fr-ca")]
|
||||
[DataRow("Any", "<ui[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, N:{N}, O:{O}]>",
|
||||
"ID:es-ES, 2:es, 3:spa, W:ESN, D:español (España), E:Spanish (Spain), N:español (España), O:es-ES")]
|
||||
[DataRow("Any", "<os[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, N:{N}, O:{O}]>",
|
||||
"ID:sv-SE, 2:sv, 3:swe, W:SVE, D:sueco (Suecia), E:Swedish (Sweden), N:svenska (Sverige), O:sv-SE")]
|
||||
// different localizations
|
||||
[DataRow("fr", "<language[D:{D@de-DE}, E:{E@de-DE}, N:{N@de-DE}, O:{O@de-DE}]>", "D:Französisch, E:French, N:français, O:fr")]
|
||||
[DataRow("fr", "<language[D:{D@pl}]>", "D:francuski")]
|
||||
[DataRow("fr", "<language[D:{D@it}]>", "D:francese")]
|
||||
public void Language_test(string language, string template, string expected)
|
||||
{
|
||||
var bookDto = Shared.GetLibraryBook();
|
||||
bookDto.Language = new CultureInfoDto(language);
|
||||
|
||||
var result = "";
|
||||
|
||||
var old = Thread.CurrentThread.CurrentCulture;
|
||||
var oldUi = Thread.CurrentThread.CurrentUICulture;
|
||||
try
|
||||
{
|
||||
Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-SE");
|
||||
Thread.CurrentThread.CurrentUICulture = new CultureInfo("es-ES");
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
result = fileTemplate
|
||||
.GetName(bookDto, new MultiConvertFileProperties { OutputFileName = string.Empty });
|
||||
}
|
||||
finally
|
||||
{
|
||||
Thread.CurrentThread.CurrentCulture = old;
|
||||
Thread.CurrentThread.CurrentUICulture = oldUi;
|
||||
}
|
||||
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
// Audible does not provide a consistent or authoritative region code for its storefronts.
|
||||
// In most cases, the storefront region can be inferred from the EnglishName of a matching RegionInfo entry. However,
|
||||
// the US and UK storefronts do not follow this pattern, and the three historical “pre‑Amazon” storefront identifiers require
|
||||
// separate interpretation to remain globally usable for all users.
|
||||
// To ensure robustness, the tests attempt to cover all known Audible storefronts explicitly.
|
||||
|
||||
// Skipping of NativeName: its output is influenced by external standards bodies and evolving globalization data (NLS vs. ICU),
|
||||
// not solely by the OSPlatform.
|
||||
// Because .NET provides no stability guarantees for NativeName across platforms or ICU/NLS versions, we do not include
|
||||
// platform-specific tests here—unlike path-related differences, which are defined and testable.
|
||||
|
||||
// test known locales
|
||||
[DataRow("us", "<locale[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, N:{N}, O:{O}, T:.{T}, L:{L}]>",
|
||||
"ID:AF2M0KC94RCEA, 2:US, 3:USA, W:USA, D:Estados Unidos, E:United States, N:United States, O:us, T:.com, L:en-US")]
|
||||
[DataRow("uk", "<locale[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, N:{N}, O:{O}, T:.{T}, L:{L}]>",
|
||||
"ID:A2I9A3Q2GNFNGQ, 2:GB, 3:GBR, W:GBR, D:Reino Unido, E:United Kingdom, N:United Kingdom, O:uk, T:.co.uk, L:en-GB")]
|
||||
[DataRow("germany", "<locale[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, N:{N}, O:{O}, T:.{T}, L:{L}]>",
|
||||
"ID:AN7V1F1VY261K, 2:DE, 3:DEU, W:DEU, D:Alemania, E:Germany, N:Deutschland, O:germany, T:.de, L:de-DE")]
|
||||
// Skip NativeName (see above)
|
||||
[DataRow("france", "<locale[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, O:{O}, T:.{T}, L:{L}]>", "ID:A2728XDNODOQ8T, 2:FR, 3:FRA, W:FRA, D:Francia, E:France, O:france, T:.fr, L:fr-FR")]
|
||||
[DataRow("australia",
|
||||
"<locale[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, O:{O}, T:.{T}, L:{L}]>", "ID:AN7EY7DTAW63G, 2:AU, 3:AUS, W:AUS, D:Australia, E:Australia, O:australia, T:.com.au, L:en-AU")]
|
||||
[DataRow("india", "<locale[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, O:{O}, T:.{T}, L:{L}]>", "ID:AJO3FBRUE6J4S, 2:IN, 3:IND, W:IND, D:India, E:India, O:india, T:.in, L:en-IN")]
|
||||
[DataRow("spain", "<locale[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, O:{O}, T:.{T}, L:{L}]>", "ID:ALMIKO4SZCSAR, 2:ES, 3:ESP, W:ESP, D:España, E:Spain, O:spain, T:.es, L:es-ES")]
|
||||
[DataRow("italy", "<locale[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, O:{O}, T:.{T}, L:{L}]>", "ID:A2N7FU2W2BU2ZC, 2:IT, 3:ITA, W:ITA, D:Italia, E:Italy, O:italy, T:.it, L:it-IT")]
|
||||
[DataRow("canada", "<locale[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, O:{O}, T:.{T}, L:{L}]>", "ID:A2CQZ5RBY40XE, 2:CA, 3:CAN, W:CAN, D:Canadá, E:Canada, O:canada, T:.ca, L:en-CA")]
|
||||
[DataRow("japan", "<locale[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, O:{O}, T:.{T}, L:{L}]>", "ID:A1QAP3MOU4173J, 2:JP, 3:JPN, W:JPN, D:Japón, E:Japan, O:japan, T:.co.jp, L:ja-JP")]
|
||||
[DataRow("brazil", "<locale[ID:{ID}, 2:{I}, 3:{I3}, W:{W}, D:{D}, E:{E}, O:{O}, T:.{T}, L:{L}]>", "ID:A10J1VAYUDTYRN, 2:BR, 3:BRA, W:BRA, D:Brasil, E:Brazil, O:brazil, T:.com.br, L:pt-BR")]
|
||||
|
||||
// test historical locales
|
||||
[DataRow("pre-amazon - us", "<locale[ID:{ID}, O:{O}, T:.{T}, L:{L}]>", "ID:AF2M0KC94RCEA, O:pre-amazon - us, T:.com, L:en-US")]
|
||||
[DataRow("pre-amazon - uk", "<locale[ID:{ID}, O:{O}, T:.{T}, L:{L}]>", "ID:A2I9A3Q2GNFNGQ, O:pre-amazon - uk, T:.co.uk, L:en-GB")]
|
||||
[DataRow("pre-amazon - germany", "<locale[ID:{ID}, O:{O}, T:.{T}, L:{L}]>", "ID:AN7V1F1VY261K, O:pre-amazon - germany, T:.de, L:de-DE")]
|
||||
|
||||
// test upcoming locales
|
||||
[DataRow("be", "<locale[ID:{ID}, E:{E}, O:{O}]>", "ID:, E:Belgium, O:be")]
|
||||
[DataRow("nl", "<locale[ID:{ID}, E:{E}, O:{O}]>", "ID:, E:Netherlands, O:nl")]
|
||||
[DataRow("se", "<locale[ID:{ID}, E:{E}, O:{O}]>", "ID:, E:Sweden, O:se")]
|
||||
[DataRow("pl", "<locale[ID:{ID}, E:{E}, O:{O}]>", "ID:, E:Poland, O:pl")]
|
||||
[DataRow("ie", "<locale[ID:{ID}, E:{E}, O:{O}]>", "ID:, E:Ireland, O:ie")]
|
||||
[DataRow("sg", "<locale[ID:{ID}, E:{E}, O:{O}]>", "ID:, E:Singapore, O:sg")]
|
||||
[DataRow("za", "<locale[ID:{ID}, E:{E}, O:{O}]>", "ID:, E:South Africa, O:za")]
|
||||
[DataRow("ae", "<locale[ID:{ID}, E:{E}, O:{O}]>", "ID:, E:United Arab Emirates, O:ae")]
|
||||
[DataRow("sa", "<locale[ID:{ID}, E:{E}, O:{O}]>", "ID:, E:Saudi Arabia, O:sa")]
|
||||
[DataRow("eg", "<locale[ID:{ID}, E:{E}, O:{O}]>", "ID:, E:Egypt, O:eg")]
|
||||
// Skip EnglishName: the official English name of Turkey changed to 'Türkiye', and the returned value now depends on
|
||||
// the OS/globalization provider (Windows-NLS vs. ICU). Tests would not be stable.
|
||||
// A future lookup may still need to account for whichever English name Audible chooses to use.
|
||||
[DataRow("tr", "<locale[ID:{ID}, E:---, O:{O}]>", "ID:, E:---, O:tr")]
|
||||
|
||||
// test some different localizations - should change only D(isplayNames)
|
||||
[DataRow("fr", "<locale[D:{D@de-DE}, E:{E@de-DE}, N:{N@de-DE}, O:{O@de-DE}]>", "D:Frankreich, E:France, N:France, O:fr")]
|
||||
[DataRow("fr", "<locale[D:{D@pl}]>", "D:Francja")]
|
||||
[DataRow("fr", "<locale[D:{D@it}]>", "D:Francia")]
|
||||
public void Locale_test(string country, string template, string expected)
|
||||
{
|
||||
var bookDto = Shared.GetLibraryBook();
|
||||
bookDto.Locale = new LocaleDto(country);
|
||||
|
||||
var result = "";
|
||||
|
||||
var old = Thread.CurrentThread.CurrentCulture;
|
||||
var oldUi = Thread.CurrentThread.CurrentUICulture;
|
||||
try
|
||||
{
|
||||
Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR");
|
||||
Thread.CurrentThread.CurrentUICulture = new CultureInfo("es-ES");
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
result = fileTemplate
|
||||
.GetName(bookDto, new MultiConvertFileProperties { OutputFileName = string.Empty });
|
||||
}
|
||||
finally
|
||||
{
|
||||
Thread.CurrentThread.CurrentCulture = old;
|
||||
Thread.CurrentThread.CurrentUICulture = oldUi;
|
||||
}
|
||||
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ These are the naming template tags currently supported by Libation.
|
||||
These tags will be replaced in the template with the audiobook's values.
|
||||
|
||||
| Tag | Description | Type |
|
||||
| ------------------------ | -------------------------------------------------------------- | -------------------------------------- |
|
||||
|--------------------------|----------------------------------------------------------------| -------------------------------------- |
|
||||
| \<id\> **†** | Audible book ID (ASIN) | Text |
|
||||
| \<title\> | Full title with subtitle | [Text](#text-formatters) |
|
||||
| \<title short\> | Title. Stop at first colon | [Text](#text-formatters) |
|
||||
@@ -37,10 +37,12 @@ These tags will be replaced in the template with the audiobook's values.
|
||||
| \<account nickname\> | Audible account nickname of this book | [Text](#text-formatters) |
|
||||
| \<tag\> | Tag(s) | [Text List](#text-list-formatters) |
|
||||
| \<first tag\> | First tag | [Text](#text-formatters) |
|
||||
| \<locale\> | Region/country | [Text](#text-formatters) |
|
||||
| \<locale\> | Region/country | [Region](#region-formatters) |
|
||||
| \<year\> | Year published | [Number](#number-formatters) |
|
||||
| \<language\> | Book's language | [Text](#text-formatters) |
|
||||
| \<language\> | Book's language | [Language](#language-formatters) |
|
||||
| \<language short\> **†** | Book's language abbreviated. Eg: ENG | Text |
|
||||
| \<os\> | Language currently set in the operating system | [Language](#language-formatters) |
|
||||
| \<ui\> | User interface language | [Language](#language-formatters) |
|
||||
| \<file date\> | File creation date/time. | [DateTime](#date-formatters) |
|
||||
| \<pub date\> | Audiobook publication date | [DateTime](#date-formatters) |
|
||||
| \<date added\> | Date the book added to your Audible account | [DateTime](#date-formatters) |
|
||||
@@ -106,7 +108,7 @@ And this example will customize the title based on whether the book has a subtit
|
||||
|
||||
## Tag Formatters
|
||||
|
||||
**Text**, **Name List**, **Number**, and **DateTime** tags can be optionally formatted using format text in square brackets after the tag name. Below is a list of supported formatters for each tag type.
|
||||
**Text**, **Name**, **Series**, **Number**, **TimeSpan**, **DateTime**, **Region**, **Language** and their **List** tags can be optionally formatted using format text in square brackets after the tag name. Below is a list of supported formatters for each tag type.
|
||||
|
||||
### Text Formatters
|
||||
|
||||
@@ -185,7 +187,7 @@ Here, a number format is inserted for the desired part in accordance with [Micro
|
||||
|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|-------------------|
|
||||
| D | A number format with "D" instead of "0". Using this will output the total number of days and reduce the amount of minutes avalable for "H" and "M". | \<minutes[DD]\> | 02 |
|
||||
| H | A number format with "H" instead of "0". Using this will output the total number of hours and reduce the amount of minutes available for "M". | \<minutes[HH]\> | 62 |
|
||||
| M | A number format with "H" instead of "0". Using this will output the total number of minutes. | \<minutes[#,#MM]\> | 3,762 |
|
||||
| M | A number format with "M" instead of "0". Using this will output the total number of minutes. | \<minutes[#,#MM]\> | 3,762 |
|
||||
| D H M | A combination of the above. | \<minutes[D'days 'MM'minutes']\> | 02days 882minutes |
|
||||
|
||||
### Number Formatters
|
||||
@@ -220,15 +222,35 @@ You can use custom formatters to construct customized DateTime string. For more
|
||||
|dd|2-digit day of the month|\<file date[yyyy-MM-dd]\>|2023-02-14|
|
||||
|HH<br>mm|The hour, using a 24-hour clock from 00 to 23<br>The minute, from 00 through 59.|\<file date[HH:mm]\>|14:45|
|
||||
|
||||
### Region Formatters
|
||||
|
||||
You can specify which part of a region you are interested in.
|
||||
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
|-------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------|---------------------------------------|
|
||||
| \{O \| I \| I2 \| I3 \| E \| N \| W \| L \| T \| ID\} | Formats the region using<br>the region part tags.<br>\{O:[Text_Formatter](#text-formatters)\} = Region as used in Libation<br>\{I:[Text_Formatter](#text-formatters)\} = Two letter ISO code<br>\{I2:[Text_Formatter](#text-formatters)\} = Two letter ISO code<br>\{I3:[Text_Formatter](#text-formatters)\} = Three letter ISO code<br>\{E:[Text_Formatter](#text-formatters)\} = English name<br>\{N:[Text_Formatter](#text-formatters)\} = Native name - OS dependent<br>\{W:[Text_Formatter](#number-formatters)\} = Unique Windows code<br>\{L:[Text_Formatter](#text-formatters)\} = Lang code used for this region/store<br>\{T:[Text_Formatter](#number-formatters)\} = TLD under which the audible store is hosted<br>\{ID:[Text_Formatter](#text-formatters)\} = Region code<br> <br><br>Formatter parts are optional and introduced by the colon. If specified the string will be used to format the part using the corresponding formatter.<br><br>Default is \{O\} | `<locale[{I} ({E})]>`<hr>`www.audible.<locale[{T}]>` | US (United States)<hr>www.audible.com |
|
||||
| \{D\} **†** | Display name interpreted by the current language settings.<br>To ensure output in a specific language the lang-code to use might be specified with a leading '@'.<br>Formatter part is also optional and introduced by the colon.<br>\{D@LANG:[Text_Formatter](#text-formatters)\} | `<locale[{D@es:u}]>` | ESTADOS UNIDOS |
|
||||
|
||||
**†** LANG may be any code from the [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) standard like `es` for Spanish, `en` for English, `de` for German, etc. or even a [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag) like 'fr-CA'.
|
||||
|
||||
### Language Formatters
|
||||
|
||||
You can specify which part of a language you are interested in.
|
||||
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------|----------------|
|
||||
| \{O \| I \| I2 \| I3 \| E \| N \| W \| ID\} | Formats the language using<br>the language part tags.<br>\{O:[Text_Formatter](#text-formatters)\} = Language as provided by audible<br>\{I:[Text_Formatter](#text-formatters)\} = Two letter ISO code<br>\{I2:[Text_Formatter](#text-formatters)\} = Two letter ISO code<br>\{I3:[Text_Formatter](#text-formatters)\} = Three letter ISO code<br>\{E:[Text_Formatter](#text-formatters)\} = English name<br>\{N:[Text_Formatter](#text-formatters)\} = Native name - OS dependent<br>\{W:[Text_Formatter](#number-formatters)\} = Unique Windows code<br>\{ID:[Text_Formatter](#text-formatters)\} = Lang code<br><br>Formatter parts are optional and introduced by the colon. If specified the string will be used to format the part using the corresponding formatter.<br><br>Default is \{O\} | `<language[{I3:l} ({E})]>` | fra (French) |
|
||||
| \{D\} | Display name interpreted by the current language settings.<br>To ensure output in a specific language the lang-code to use might be specified with a leading '@'.<br>Formatter part is also optional and introduced by the colon.<br>\{D@LANG:[Text_Formatter](#text-formatters)\} | `<language[{D@es}]>` | francés |
|
||||
|
||||
### Checks
|
||||
|
||||
| Check-Pattern | Description | Example |
|
||||
| --------------- | ------------------------------------------------------------------------------- | --------------------------------------- |
|
||||
| =STRING **†** | Matches if one item is equal to STRING (case ignored) | \<is tag[=Tag1]-\> |
|
||||
| !=STRING **†** | Matches if one item is not equal to STRING (case ignored) | \<is first author[!=Arthur]-\> |
|
||||
| ~STRING **†** | Matches if one items is matched by the regular expression STRING (case ignored) | \<is title[~(\[XYZ\]).*\\1]-\> |
|
||||
| #=NUMBER **‡** | Matches if the number value is equal to NUMBER | \<is channels[#=2]-\> |
|
||||
| #!=NUMBER **‡** | Matches if the number value is not equal to NUMBER | \<is author[#!=1]-\> |
|
||||
| Check-Pattern | Description | Example |
|
||||
| ---------------- | ------------------------------------------------------------------------------- | ------------------------------------------ |
|
||||
| =STRING **†** | Matches if one item is equal to STRING (case ignored) | \<is tag[=Tag1]-\> |
|
||||
| !=STRING **†** | Matches if one item is not equal to STRING (case ignored) | \<is first author[!=Arthur]-\> |
|
||||
| ~STRING **†** | Matches if one items is matched by the regular expression STRING (case ignored) | \<is title[~(\[XYZ\]).*\\1]-\> |
|
||||
| #=NUMBER **‡** | Matches if the number value is equal to NUMBER | \<is channels[#=2]-\> |
|
||||
| #!=NUMBER **‡** | Matches if the number value is not equal to NUMBER | \<is author[#!=1]-\> |
|
||||
| #\>=NUMBER **‡** | Matches if the number value is greater than or equal to NUMBER | \<is bitrate[#\>=128]-\> |
|
||||
| #\>NUMBER **‡** | Matches if the number value is greater than NUMBER | \<is title[#\>30]-\> |
|
||||
| #\<=NUMBER **‡** | Matches if the number value is less than or equal to NUMBER | \<is first narrator[format({F})][#\<=1]-\> |
|
||||
|
||||
Reference in New Issue
Block a user