1 module creator.core.i18n;
2 import creator.core;
3 import i18n.culture;
4 import i18n;
5 import i18n.tr;
6 import std.file;
7 import std.path;
8 import std.string;
9 import std.algorithm : sort;
10 import std.uni : icmp;
11 
12 /+
13     HACK: This little comment tricks genpot to generate our LANG_NAME entry.
14 
15     // The name of the language this translation is a translation to
16     // in the native script of the language (for the region)
17     // Eg. this would be "Dansk" for Danish and "日本語" for Japanese.
18     _("LANG_NAME")
19 +/
20 
21 private {
22     TLEntry[] localeFiles;
23 
24     string incGetCultureExpression(string langcode) {
25 
26         // Most cases
27         foreach(locale; localeFiles) {
28             if (locale.code == langcode) {
29                 return locale.humanName;
30             }
31         }
32 
33         // Fallback
34         if (langcode.length >= 5) {
35             return format("%s (%s)", i18nGetCultureLanguage(langcode),
36                 langcode == "zh-CN" ? "Simplified" : 
37                 langcode == "zh-TW" ? "Traditional" :
38                 i18nGetCultureCountry(langcode));
39         }
40         return i18nGetCultureLanguage(langcode);
41     }
42 
43     void incLocaleScan(string path) {
44 
45         // Skip non-existent paths
46         if (!path.exists) return;
47 
48         foreach(DirEntry entry; dirEntries(path, "*.mo", SpanMode.shallow)) {
49             
50             // Get langcode from filename
51             string langcode = baseName(stripExtension(entry.name));
52 
53             // Skip langcodes we don't know
54             if (!i18nValidateCultureCode(langcode)) continue;
55 
56             string langName = i18nGetLanguageName(entry.name);
57             if (langName == "<UNKNOWN LANGUAGE>") langName = incGetCultureExpression(langcode);
58             
59             // Add locale
60             localeFiles ~= TLEntry(
61                 langName,
62                 langName.toStringz,
63                 langcode, 
64                 entry.name,
65                 path
66             );
67         }
68     }
69 }
70 
71 /**
72     Entry in the translations table
73 */
74 struct TLEntry {
75 public:
76     string humanName;
77     const(char)* humanNameC;
78     string code;
79     string file;
80     string path;
81 }
82 
83 /**
84     Initialize translations
85 */
86 void incLocaleInit() {
87 
88     // These exist for testing + user added localization
89     incLocaleScan(incGetAppLocalePath());
90     incLocaleScan(thisExePath().dirName);
91 
92     // On Windows we want to store locales next to the exe file in a i18n folder
93     version(Windows) incLocaleScan(buildPath(thisExePath().dirName, "i18n"));
94     
95     // On macOS we store the locale in the app bundle under the Resources subdirectory.
96     version(OSX) incLocaleScan(buildPath(thisExePath().dirName, "../Resources/i18n"));
97     
98     // Some distribution platforms like AppImage has its own locale path
99     // this is here to detect it and add it in to the scan area.
100     auto extraLocalePath = incGetAppLocalePathExtra();
101     if (extraLocalePath) incLocaleScan(extraLocalePath);
102     
103     // sort the files by human readable name
104     localeFiles.sort!(compareEntries);
105     //disambiguate locales with the same human name
106     markDups(localeFiles);
107 }
108 
109 bool compareEntries(TLEntry a, TLEntry b) {
110     int cmp = icmp(a.humanName, b.humanName);
111     if (cmp == 0) {
112         return a.path < b.path;
113     }
114     return cmp < 0;
115 }
116 
117 /**
118     Disambiguate by source folder for TLEntrys with identical humanNames.
119     Expects an array sorted by humanName
120 */
121 void markDups(TLEntry[] entries) {
122 
123     // Skip if only one entry
124     if (entries.length <= 1) return;
125     
126     TLEntry* prevEntry = &entries[0];
127     bool prevIsDup = false;
128 
129     foreach(ref entry; entries[1 .. $]) {
130         bool entryIsDup = entry.humanName == prevEntry.humanName;
131 
132         // If prevEntry has same humanName as entry before prevEntry, or as this entry,
133         // disambiguate with the source folder
134         if (prevIsDup || entryIsDup) {
135             prevEntry.humanName ~= " (" ~ prevEntry.path ~ ")";
136             prevEntry.humanNameC = prevEntry.humanName.toStringz;
137         }
138         prevIsDup = entryIsDup;
139         prevEntry = &entry;
140     }
141 
142     if (prevIsDup) {
143         prevEntry.humanName ~= " (" ~ prevEntry.path ~ ")";
144         prevEntry.humanNameC = prevEntry.humanName.toStringz;
145     }
146 }
147 
148 /**
149     Gets the current selected locale human name
150 */
151 string incLocaleCurrentName() {
152     string code = incSettingsGet("lang", "en");
153     string currCode = code.length == 0 ? "en": code;
154     return incGetCultureExpression(currCode);
155 }
156 
157 /**
158     Sets the locale for the application
159 */
160 void incLocaleSet(string code) {
161     incSettingsSet("lang", code);
162     
163     // Builtin EN has no .po file
164     if (code.length == 0 || code == "en") {
165         i18nClearLanguage();
166         return;
167     }
168 
169     // Other languages do, though.
170     i18nLoadLanguage(incLocaleGetEntryFor(code).file);
171 }
172 
173 /**
174     Get locale entry for a code
175 */
176 TLEntry* incLocaleGetEntryFor(string code) {
177     foreach(ref entry; localeFiles) {
178         if (entry.code == code) return &entry;
179     }
180     return null;
181 }
182 
183 /**
184     Returns the locale list
185 */
186 TLEntry[] incLocaleGetEntries() {
187     return localeFiles;
188 }