6 using System.Collections.Generic;
7 using System.Collections.Concurrent;
8 using System.Collections.Immutable;
10 using System.Xml.Linq;
11 using System.Globalization;
12 using System.Text.Unicode;
21 public static class TextManager
23 public static bool DebugDraw;
25 public readonly
static LanguageIdentifier DefaultLanguage =
"English".ToLanguageIdentifier();
26 public readonly
static ConcurrentDictionary<LanguageIdentifier, ImmutableList<TextPack>> TextPacks =
new ConcurrentDictionary<LanguageIdentifier, ImmutableList<TextPack>>();
27 public static IEnumerable<LanguageIdentifier> AvailableLanguages => TextPacks.Keys;
29 private readonly
static Dictionary<Identifier, WeakReference<TagLString>> cachedStrings =
30 new Dictionary<Identifier, WeakReference<TagLString>>();
31 private static ImmutableHashSet<Identifier> nonCacheableTags =
32 ImmutableHashSet<Identifier>.Empty;
34 public static int LanguageVersion {
get;
private set; } = 0;
36 private static ImmutableArray<Range<int>> UnicodeToIntRanges(params UnicodeRange[] ranges)
38 .Select(r =>
new Range<int>(r.FirstCodePoint, r.FirstCodePoint + r.Length - 1))
39 .OrderBy(r => r.Start)
43 public enum SpeciallyHandledCharCategory
54 public static readonly ImmutableArray<SpeciallyHandledCharCategory> SpeciallyHandledCharCategories
55 = Enum.GetValues<SpeciallyHandledCharCategory>()
56 .Where(c => c is not (SpeciallyHandledCharCategory.None or SpeciallyHandledCharCategory.All))
59 private static readonly ImmutableDictionary<SpeciallyHandledCharCategory, ImmutableArray<Range<int>>> SpeciallyHandledCharacterRanges
62 (SpeciallyHandledCharCategory.CJK, UnicodeToIntRanges(
63 UnicodeRanges.HalfwidthandFullwidthForms,
64 UnicodeRanges.HangulJamo,
65 UnicodeRanges.HangulCompatibilityJamo,
66 UnicodeRanges.CjkRadicalsSupplement,
67 UnicodeRanges.CjkSymbolsandPunctuation,
68 UnicodeRanges.EnclosedCjkLettersandMonths,
69 UnicodeRanges.CjkCompatibility,
70 UnicodeRanges.CjkUnifiedIdeographsExtensionA,
71 UnicodeRanges.CjkUnifiedIdeographs,
72 UnicodeRanges.HangulSyllables,
73 UnicodeRanges.CjkCompatibilityForms,
75 UnicodeRanges.BlockElements
77 (SpeciallyHandledCharCategory.Japanese, UnicodeToIntRanges(
78 UnicodeRanges.Hiragana,
79 UnicodeRanges.Katakana
81 (SpeciallyHandledCharCategory.Cyrillic, UnicodeToIntRanges(
82 UnicodeRanges.Cyrillic,
83 UnicodeRanges.CyrillicSupplement,
84 UnicodeRanges.CyrillicExtendedA,
85 UnicodeRanges.CyrillicExtendedB,
86 UnicodeRanges.CyrillicExtendedC
88 }.ToImmutableDictionary();
90 public static SpeciallyHandledCharCategory GetSpeciallyHandledCategories(LocalizedString text)
91 => GetSpeciallyHandledCategories(text.Value);
93 public static SpeciallyHandledCharCategory GetSpeciallyHandledCategories(
string text)
95 if (
string.IsNullOrEmpty(text)) {
return SpeciallyHandledCharCategory.None; }
97 var retVal = SpeciallyHandledCharCategory.None;
98 for (
int i = 0; i < text.Length; i++)
102 foreach (var category
in SpeciallyHandledCharCategories)
104 if (retVal.HasFlag(category)) {
continue; }
106 for (
int j = 0; j < SpeciallyHandledCharacterRanges[category].Length; j++)
108 var range = SpeciallyHandledCharacterRanges[category][j];
113 if (chr < range.Start) {
break; }
116 if (range.Contains(chr))
124 if (retVal == SpeciallyHandledCharCategory.All)
129 return SpeciallyHandledCharCategory.All;
135 public static bool IsCJK(LocalizedString text)
136 => IsCJK(text.Value);
138 public static bool IsCJK(
string text)
139 => GetSpeciallyHandledCategories(text).HasFlag(SpeciallyHandledCharCategory.CJK);
144 public static void VerifyLanguageAvailable()
146 if (!TextPacks.ContainsKey(GameSettings.CurrentConfig.Language))
148 DebugConsole.ThrowError($
"Could not find the language \"{GameSettings.CurrentConfig.Language}\". Trying to switch to English...");
149 var config = GameSettings.CurrentConfig;
150 config.Language =
"English".ToLanguageIdentifier();;
151 GameSettings.SetCurrentConfig(config);
155 public static bool ContainsTag(
string tag)
157 return ContainsTag(tag.ToIdentifier());
160 public static bool ContainsTag(Identifier tag)
162 return TextPacks[GameSettings.CurrentConfig.Language].Any(p => p.Texts.ContainsKey(tag));
165 public static bool ContainsTag(Identifier tag, LanguageIdentifier language)
167 return TextPacks[language].Any(p => p.Texts.ContainsKey(tag));
169 public static IEnumerable<string> GetAll(
string tag)
170 => GetAll(tag.ToIdentifier());
172 public static IEnumerable<string> GetAll(Identifier tag)
174 var allTexts = TextPacks[GameSettings.CurrentConfig.Language]
175 .SelectMany(p => p.Texts.TryGetValue(tag, out var value)
176 ? (IEnumerable<TextPack.Text>)value
177 : Array.Empty<TextPack.Text>()).ToList();
179 var firstOverride = allTexts.FirstOrDefault(t => t.IsOverride);
180 if (firstOverride !=
default)
182 return allTexts.Where(t => t.IsOverride && t.TextPack == firstOverride.TextPack).Select(t => t.String);
186 return allTexts.Select(t => t.String);
190 public static IEnumerable<KeyValuePair<Identifier, string>> GetAllTagTextPairs()
192 var allTexts = TextPacks[GameSettings.CurrentConfig.Language]
193 .SelectMany(p => p.Texts);
195 foreach (var textList
in allTexts)
197 var firstOverride = textList.Value.FirstOrDefault(t => t.IsOverride);
198 if (firstOverride !=
default)
201 foreach (var text
in textList.Value)
203 if (!text.IsOverride) {
continue; }
204 yield
return new KeyValuePair<Identifier, string>(textList.Key, text.String);
209 foreach (var text
in textList.Value)
211 yield
return new KeyValuePair<Identifier, string>(textList.Key, text.String);
217 public static IEnumerable<string> GetTextFiles()
219 return GetTextFilesRecursive(Path.Combine(
"Content",
"Texts"));
222 private static IEnumerable<string> GetTextFilesRecursive(
string directory)
224 foreach (
string file
in Directory.GetFiles(directory))
226 yield
return file.CleanUpPath();
228 foreach (
string subDir
in Directory.GetDirectories(directory))
230 foreach (
string file
in GetTextFilesRecursive(subDir))
237 public static string GetTranslatedLanguageName(LanguageIdentifier languageIdentifier)
239 return TextPacks[languageIdentifier].First().TranslatedName;
242 public static void ClearCache()
246 cachedStrings.Clear();
247 nonCacheableTags.Clear();
251 public static LocalizedString Get(params Identifier[] tags)
253 if (tags.Length == 1)
257 return new TagLString(tags);
260 public static LocalizedString Get(Identifier tag)
262 TagLString? str =
null;
265 if (!nonCacheableTags.Contains(tag))
267 if (cachedStrings.TryGetValue(tag, out var strRef))
269 if (!strRef.TryGetTarget(out str))
271 cachedStrings.Remove(tag);
275 if (str is
null && TextPacks.ContainsKey(GameSettings.CurrentConfig.Language))
278 foreach (var pack
in TextPacks[GameSettings.CurrentConfig.Language])
280 if (pack.Texts.TryGetValue(tag, out var texts))
282 count += texts.Length;
283 if (count > 1) {
break; }
289 nonCacheableTags = nonCacheableTags.Add(tag);
293 str =
new TagLString(tag);
294 cachedStrings.Add(tag,
new WeakReference<TagLString>(str));
299 return str ??
new TagLString(tag);
302 public static LocalizedString Get(
string tag)
303 => Get(tag.ToIdentifier());
305 public static LocalizedString Get(params
string[] tags)
306 => Get(tags.ToIdentifiers());
308 public static LocalizedString AddPunctuation(
char punctuationSymbol, params LocalizedString[] texts)
310 return new AddedPunctuationLString(punctuationSymbol, texts);
313 public static LocalizedString GetFormatted(Identifier tag, params
object[] args)
315 return GetFormatted(
new TagLString(tag), args);
318 public static LocalizedString GetFormatted(LocalizedString str, params
object[] args)
320 LocalizedString[] argStrs =
new LocalizedString[args.Length];
321 for (
int i = 0; i < args.Length; i++)
323 if (args[i] is LocalizedString ls) { argStrs[i] = ls; }
324 else { argStrs[i] =
new RawLString(args[i].ToString() ??
""); }
326 return new FormattedLString(str, argStrs);
329 public static string FormatServerMessage(
string str) => $
"{str}~";
331 public static string FormatServerMessage(
string message, params (
string Key,
string Value)[] keysWithValues)
333 if (keysWithValues.Length == 0)
335 return FormatServerMessage(message);
337 var startIndex = message.LastIndexOf(
'/') + 1;
338 var endIndex = message.IndexOf(
'~', startIndex);
341 endIndex = message.Length - 1;
343 var textId = message.Substring(startIndex, endIndex - startIndex + 1);
344 var prefixEntries = keysWithValues.Select((kv, index) =>
346 if (kv.Value.IndexOfAny(
new char[] {
'~',
'/' }) != -1)
348 var kvStartIndex = kv.Value.LastIndexOf(
'/') + 1;
349 return kv.Value.Substring(0, kvStartIndex) + $
"[{textId}.{index}]={kv.Value.Substring(kvStartIndex)}";
355 }).Where(e => e !=
null).ToArray();
356 return string.Join(
"",
357 (prefixEntries.Length > 0 ?
string.Join(
"/", prefixEntries) +
"/" :
""),
359 string.Join(
"", keysWithValues.Select((kv, index) => kv.Value.IndexOfAny(
new char[] {
'~',
'/' }) != -1 ? $
"~{kv.Key}=[{textId}.{index}]" : $
"~{kv.Key}={kv.Value}"))
363 internal static string FormatServerMessageWithPronouns(CharacterInfo charInfo,
string message, params (
string Key,
string Value)[] keysWithValues)
365 var pronounCategory = charInfo.Prefab.Pronouns;
366 (
string Key,
string Value)[] pronounKwv =
new[]
368 (
"[PronounLowercase]", charInfo.ReplaceVars($
"Pronoun[{pronounCategory}]Lowercase")),
369 (
"[PronounUppercase]", charInfo.ReplaceVars($
"Pronoun[{pronounCategory}]")),
370 (
"[PronounPossessiveLowercase]", charInfo.ReplaceVars($
"PronounPossessive[{pronounCategory}]Lowercase")),
371 (
"[PronounPossessiveUppercase]", charInfo.ReplaceVars($
"PronounPossessive[{pronounCategory}]")),
372 (
"[PronounReflexiveLowercase]", charInfo.ReplaceVars($
"PronounReflexive[{pronounCategory}]Lowercase")),
373 (
"[PronounReflexiveUppercase]", charInfo.ReplaceVars($
"PronounReflexive[{pronounCategory}]"))
375 return FormatServerMessage(message, keysWithValues.Concat(pronounKwv).ToArray());
379 public static string JoinServerMessages(
string separator,
string[] parts,
string namePrefix =
"part.")
382 return string.Join(
"/",
383 string.Join(
"/", parts.Select((part, index) =>
385 var partStart = part.LastIndexOf(
'/') + 1;
386 return partStart > 0 ? $
"{part.Substring(0, partStart)}/[{namePrefix}{index}]={part.Substring(partStart)}" : $
"[{namePrefix}{index}]={part.Substring(partStart)}";
388 string.Join(separator, parts.Select((part, index) => $
"[{namePrefix}{index}]")));
391 public static LocalizedString ParseInputTypes(LocalizedString str,
bool useColorHighlight =
false)
393 return new InputTypeLString(str, useColorHighlight);
396 public static LocalizedString GetWithVariable(
string tag,
string varName, LocalizedString value,
FormatCapitals formatCapitals =
FormatCapitals.No)
398 return GetWithVariable(tag.ToIdentifier(), varName.ToIdentifier(), value, formatCapitals);
401 public static LocalizedString GetWithVariable(Identifier tag, Identifier varName, LocalizedString value,
FormatCapitals formatCapitals =
FormatCapitals.No)
403 return GetWithVariables(tag, (varName, value));
406 public static LocalizedString GetWithVariables(
string tag, params (
string Key,
string Value)[] replacements)
408 return GetWithVariables(
410 replacements.Select(kv =>
411 (kv.Key.ToIdentifier(),
412 (LocalizedString)
new RawLString(kv.Value),
416 public static LocalizedString GetWithVariables(
string tag, params (
string Key, LocalizedString Value)[] replacements)
418 return GetWithVariables(
420 replacements.Select(kv =>
421 (kv.Key.ToIdentifier(),
426 public static LocalizedString GetWithVariables(
string tag, params (
string Key, LocalizedString Value,
FormatCapitals FormatCapitals)[] replacements)
428 return GetWithVariables(
430 replacements.Select(kv =>
431 (kv.Key.ToIdentifier(),
433 kv.FormatCapitals)));
436 public static LocalizedString GetWithVariables(
string tag, params (
string Key,
string Value,
FormatCapitals FormatCapitals)[] replacements)
438 return GetWithVariables(
440 replacements.Select(kv =>
441 (kv.Key.ToIdentifier(),
442 (LocalizedString)
new RawLString(kv.Value),
443 kv.FormatCapitals)));
446 public static LocalizedString GetWithVariables(Identifier tag, params (Identifier Key, LocalizedString Value)[] replacements)
448 return GetWithVariables(tag, replacements.Select(kv => (kv.Key, kv.Value,
FormatCapitals.No)));
451 public static LocalizedString GetWithVariables(Identifier tag, IEnumerable<(Identifier, LocalizedString,
FormatCapitals)> replacements)
453 return new ReplaceLString(
new TagLString(tag), StringComparison.OrdinalIgnoreCase, replacements);
456 public static void ConstructDescription(ref LocalizedString description, XElement descriptionElement, Func<string, string>? customTagReplacer =
null)
465 Identifier tag = descriptionElement.GetAttributeIdentifier(
"tag", Identifier.Empty);
466 LocalizedString extraDescriptionLine = Get(tag).Fallback(tag.Value);
467 foreach (XElement replaceElement
in descriptionElement.Elements())
469 if (replaceElement.NameAsIdentifier() !=
"replace") {
continue; }
471 Identifier variableTag = replaceElement.GetAttributeIdentifier(
"tag", Identifier.Empty);
472 LocalizedString replacementValue =
string.Empty;
473 if (customTagReplacer !=
null)
475 replacementValue = customTagReplacer(replaceElement.GetAttributeString(
"value",
string.Empty));
477 if (replacementValue.IsNullOrWhiteSpace())
479 string[] replacementValues = replaceElement.GetAttributeStringArray(
"value", Array.Empty<
string>());
480 for (
int i = 0; i < replacementValues.Length; i++)
482 replacementValue += Get(replacementValues[i]).Fallback(replacementValues[i]);
483 if (i < replacementValues.Length - 1)
485 replacementValue +=
", ";
489 if (replaceElement.Attribute(
"color") !=
null)
491 string colorStr = replaceElement.GetAttributeString(
"color",
"255,255,255,255");
492 replacementValue = $
"‖color:{colorStr}‖" + replacementValue +
"‖color:end‖";
494 extraDescriptionLine = extraDescriptionLine.Replace(variableTag, replacementValue);
496 if (!(description is RawLString { Value:
"" })) { description +=
"\n"; }
497 description += extraDescriptionLine;
500 public static LocalizedString FormatCurrency(
int amount,
bool includeCurrencySymbol =
true)
502 string valueString =
string.Format(CultureInfo.InvariantCulture,
"{0:N0}", amount);
503 return includeCurrencySymbol ?
504 GetWithVariable(
"currencyformat",
"[credits]", valueString) :
508 public static LocalizedString GetServerMessage(
string serverMessage)
510 return new ServerMsgLString(serverMessage);
513 public static LocalizedString Capitalize(
this LocalizedString str)
515 return new CapitalizeLString(str);
518 public static void IncrementLanguageVersion()
525 public static void CheckForDuplicates(LanguageIdentifier lang)
527 if (!TextPacks.ContainsKey(lang))
529 DebugConsole.ThrowError(
"No text packs available for the selected language (" + lang +
")!");
534 foreach (TextPack textPack
in TextPacks[lang])
536 textPack.CheckForDuplicates(packIndex);
541 public static void WriteToCSV()
543 LanguageIdentifier lang = DefaultLanguage;
545 if (!TextPacks.ContainsKey(lang))
547 DebugConsole.ThrowError(
"No text packs available for the selected language (" + lang +
")!");
552 foreach (TextPack textPack
in TextPacks[lang])
554 textPack.WriteToCSV(packIndex);