@quenty/clienttranslator 14.5.0 → 14.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [14.6.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/clienttranslator@14.5.0...@quenty/clienttranslator@14.6.0) (2024-09-12)
7
+
8
+
9
+ ### Features
10
+
11
+ * Unedited all changes ([60e64e3](https://github.com/Quenty/NevermoreEngine/commit/60e64e3efce17c10c4b8965871187d231b338dd4))
12
+
13
+
14
+
15
+
16
+
6
17
  # [14.5.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/clienttranslator@14.4.0...@quenty/clienttranslator@14.5.0) (2024-08-09)
7
18
 
8
19
  **Note:** Version bump only for package @quenty/clienttranslator
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/clienttranslator",
3
- "version": "14.5.0",
3
+ "version": "14.6.0",
4
4
  "description": "Gets local translator for player",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -25,18 +25,20 @@
25
25
  "Quenty"
26
26
  ],
27
27
  "dependencies": {
28
- "@quenty/blend": "^12.4.0",
29
- "@quenty/instanceutils": "^13.4.0",
30
- "@quenty/loader": "^10.3.0",
31
- "@quenty/maid": "^3.2.0",
32
- "@quenty/promise": "^10.3.0",
28
+ "@quenty/blend": "^12.5.0",
29
+ "@quenty/instanceutils": "^13.5.0",
30
+ "@quenty/loader": "^10.4.0",
31
+ "@quenty/maid": "^3.3.0",
32
+ "@quenty/promise": "^10.4.0",
33
+ "@quenty/promisemaid": "^5.4.0",
33
34
  "@quenty/pseudolocalize": "^3.3.0",
34
- "@quenty/rx": "^13.4.0",
35
+ "@quenty/rx": "^13.5.0",
35
36
  "@quenty/string": "^3.2.0",
36
- "@quenty/table": "^3.5.0"
37
+ "@quenty/table": "^3.5.0",
38
+ "@quenty/valueobject": "^13.5.0"
37
39
  },
38
40
  "publishConfig": {
39
41
  "access": "public"
40
42
  },
41
- "gitHead": "ba466bdbc05c42fb607cf5e228c16339201d21d7"
43
+ "gitHead": "fb172906f3ee725269ec1e5f4daf9dca227e729d"
42
44
  }
@@ -0,0 +1,114 @@
1
+ --[=[
2
+ Utility to build a localization table from json, intended to be used with rojo. Can also handle Rojo json
3
+ objects turned into tables!
4
+
5
+ @class LocalizationEntryParserUtils
6
+ ]=]
7
+
8
+ local HttpService = game:GetService("HttpService")
9
+ local RunService = game:GetService("RunService")
10
+
11
+ local require = require(script.Parent.loader).load(script)
12
+
13
+ local PseudoLocalize = require("PseudoLocalize")
14
+
15
+ local LocalizationEntryParserUtils = {}
16
+
17
+ function LocalizationEntryParserUtils.decodeFromInstance(tableName, sourceLocaleId, folder)
18
+ assert(type(tableName) == "string", "Bad tableName")
19
+ assert(typeof(folder) == "Instance", "Bad folder")
20
+
21
+ local lookupTable = {}
22
+ local baseKey = ""
23
+
24
+ for _, descendant in pairs(folder:GetDescendants()) do
25
+ if descendant:IsA("StringValue") then
26
+ local localeId = LocalizationEntryParserUtils._parseLocaleFromName(descendant.Name)
27
+ local decodedTable = HttpService:JSONDecode(descendant.Value)
28
+
29
+ LocalizationEntryParserUtils._parseTableToResultsList(lookupTable, sourceLocaleId, localeId, baseKey, decodedTable, tableName)
30
+ elseif descendant:IsA("ModuleScript") then
31
+ local localeId = LocalizationEntryParserUtils._parseLocaleFromName(descendant.Name)
32
+ local decodedTable = require(descendant)
33
+
34
+ LocalizationEntryParserUtils._parseTableToResultsList(lookupTable, sourceLocaleId, localeId, baseKey, decodedTable, tableName)
35
+ end
36
+ end
37
+
38
+ local results = {}
39
+ for _, item in pairs(lookupTable) do
40
+ table.insert(results, item)
41
+ end
42
+ return results
43
+ end
44
+
45
+ function LocalizationEntryParserUtils.decodeFromTable(tableName, localeId, dataTable)
46
+ assert(type(tableName) == "string", "Bad tableName")
47
+ assert(type(localeId) == "string", "Bad localeId")
48
+ assert(type(dataTable) == "table", "Bad dataTable")
49
+
50
+ local lookupTable = {}
51
+
52
+ local baseKey = ""
53
+ LocalizationEntryParserUtils._parseTableToResultsList(lookupTable, localeId, localeId, baseKey, dataTable, tableName)
54
+
55
+ local results = {}
56
+ for _, item in pairs(lookupTable) do
57
+ table.insert(results, item)
58
+ end
59
+ return results
60
+ end
61
+
62
+ function LocalizationEntryParserUtils._parseLocaleFromName(name)
63
+ if name:sub(-5) == ".json" then
64
+ return name:sub(1, #name-5)
65
+ else
66
+ return name
67
+ end
68
+ end
69
+
70
+ function LocalizationEntryParserUtils._parseTableToResultsList(lookupTable, sourceLocaleId, localeId, baseKey, dataTable, tableName)
71
+ assert(type(lookupTable) == "table", "Bad lookupTable")
72
+ assert(type(sourceLocaleId) == "string", "Bad sourceLocaleId")
73
+ assert(type(localeId) == "string", "Bad localeId")
74
+ assert(type(baseKey) == "string", "Bad baseKey")
75
+ assert(type(dataTable) == "table", "Bad dataTable")
76
+ assert(type(tableName) == "string", "Bad tableName")
77
+
78
+ for index, text in pairs(dataTable) do
79
+ local key = baseKey .. index
80
+ if type(text) == "table" then
81
+ LocalizationEntryParserUtils._parseTableToResultsList(lookupTable, sourceLocaleId, localeId, key .. ".", text, tableName)
82
+ elseif type(text) == "string" then
83
+ local found = lookupTable[key]
84
+ if found then
85
+ found.Values[localeId] = key
86
+ else
87
+ -- Guarantee the context is unique. This is important because Roblox will not
88
+ -- allow something with the same source without a differing context text.
89
+ local context = string.format("Generated from %s with key %s", tableName, key)
90
+
91
+ found = {
92
+ Context = context;
93
+ Example = text;
94
+ Key = key;
95
+ Source = sourceLocaleId;
96
+ Values = {
97
+ [localeId] = text;
98
+ };
99
+ };
100
+
101
+ if RunService:IsStudio() and sourceLocaleId == localeId then
102
+ found.Values[PseudoLocalize.getDefaultPseudoLocaleId()] = PseudoLocalize.pseudoLocalize(text)
103
+ end
104
+
105
+ lookupTable[key] = found;
106
+ end
107
+
108
+ else
109
+ error(string.format("Bad type for text at key '%s'", key))
110
+ end
111
+ end
112
+ end
113
+
114
+ return LocalizationEntryParserUtils
@@ -15,19 +15,17 @@
15
15
 
16
16
  local require = require(script.Parent.loader).load(script)
17
17
 
18
- local Players = game:GetService("Players")
19
18
  local RunService = game:GetService("RunService")
20
19
 
21
20
  local Blend = require("Blend")
22
- local JsonToLocalizationTable = require("JsonToLocalizationTable")
23
- local LocalizationServiceUtils = require("LocalizationServiceUtils")
21
+ local LocalizationEntryParserUtils = require("LocalizationEntryParserUtils")
24
22
  local Maid = require("Maid")
25
- local Observable = require("Observable")
26
- local Promise = require("Promise")
27
23
  local PseudoLocalize = require("PseudoLocalize")
28
24
  local Rx = require("Rx")
29
25
  local RxInstanceUtils = require("RxInstanceUtils")
30
26
  local TranslationKeyUtils = require("TranslationKeyUtils")
27
+ local TranslatorService = require("TranslatorService")
28
+ local ValueObject = require("ValueObject")
31
29
 
32
30
  local JSONTranslator = {}
33
31
  JSONTranslator.ClassName = "JSONTranslator"
@@ -60,28 +58,26 @@ JSONTranslator.__index = JSONTranslator
60
58
  ```
61
59
 
62
60
  @param translatorName string -- Name of the translator. Used for source.
63
- @param ... any
61
+ @param localeId string
62
+ @param dataTable table
64
63
  @return JSONTranslator
65
64
  ]=]
66
- function JSONTranslator.new(translatorName, ...)
65
+ function JSONTranslator.new(translatorName, localeId, dataTable)
66
+ assert(type(translatorName) == "string", "Bad translatorName")
67
+
67
68
  local self = setmetatable({}, JSONTranslator)
68
69
 
69
- assert(type(translatorName) == "string", "Bad translatorName")
70
+ self._translatorName = translatorName
70
71
  self.ServiceName = translatorName
71
72
 
72
- -- Cache localizaiton table, because it can take 10-20ms to load.
73
- self._localizationTable = JsonToLocalizationTable.toLocalizationTable(translatorName, ...)
74
- self._englishTranslator = self._localizationTable:GetTranslator("en")
75
- self._fallbacks = {}
76
-
77
- if RunService:IsRunning() and RunService:IsClient() then
78
- self._promiseTranslator = LocalizationServiceUtils.promiseTranslator(Players.LocalPlayer)
73
+ if type(localeId) == "string" and type(dataTable) == "table" then
74
+ self._entries = LocalizationEntryParserUtils.decodeFromTable(self._translatorName, localeId, dataTable)
75
+ elseif typeof(localeId) == "Instance" then
76
+ local parent = localeId
77
+ local sourceLocaleId = "en"
78
+ self._entries = LocalizationEntryParserUtils.decodeFromInstance(self._translatorName, sourceLocaleId, parent)
79
79
  else
80
- self._promiseTranslator = Promise.resolved(self._englishTranslator)
81
- end
82
-
83
- if RunService:IsStudio() then
84
- PseudoLocalize.addToLocalizationTable(self._localizationTable, nil, "en")
80
+ error("Must pass a localeId and dataTable")
85
81
  end
86
82
 
87
83
  return self
@@ -89,6 +85,99 @@ end
89
85
 
90
86
  function JSONTranslator:Init(serviceBag)
91
87
  self._serviceBag = assert(serviceBag, "No serviceBag")
88
+ self._translatorService = self._serviceBag:GetService(TranslatorService)
89
+
90
+ self._maid = Maid.new()
91
+ self._localTranslator = self._maid:Add(ValueObject.new(nil))
92
+ self._sourceTranslator = self._maid:Add(ValueObject.new(nil))
93
+
94
+ self._localizationTable = self._translatorService:GetLocalizationTable()
95
+
96
+ for _, item in pairs(self._entries) do
97
+ for localeId, text in pairs(item.Values) do
98
+ self._localizationTable:SetEntryValue(item.Key, item.Source, item.Context, localeId, text)
99
+ end
100
+ self._localizationTable:SetEntryExample(item.Key, item.Source, item.Context, item.Example)
101
+ end
102
+
103
+ self._maid:GiveTask(RxInstanceUtils.observeProperty(self._localizationTable, "SourceLocaleId"):Subscribe(function(localeId)
104
+ self._sourceTranslator.Value = self._localizationTable:GetTranslator(localeId)
105
+ end))
106
+ self._maid:GiveTask(self._translatorService:ObserveLocaleId():Subscribe(function(localeId)
107
+ self._localTranslator.Value = self._localizationTable:GetTranslator(localeId)
108
+ end))
109
+ end
110
+
111
+ --[=[
112
+ Observes the translated value
113
+ @param translationKey string
114
+ @param translationArgs table? -- May have observables (or convertable to observables) in it.
115
+ @return Observable<string>
116
+ ]=]
117
+ function JSONTranslator:ObserveFormatByKey(translationKey, translationArgs)
118
+ assert(self ~= JSONTranslator, "Construct a new version of this class to use it")
119
+ assert(type(translationKey) == "string", "Key must be a string")
120
+
121
+ return Rx.combineLatest({
122
+ translator = self:ObserveTranslator();
123
+ translationKey = translationKey;
124
+ translationArgs = self:_observeArgs(translationArgs);
125
+ }):Pipe({
126
+ Rx.switchMap(function(mainState)
127
+ if mainState.translator then
128
+ return Rx.of(self:_doTranslation(mainState.translator, mainState.translationKey, mainState.translationArgs))
129
+ end
130
+
131
+ -- Fall back to local or source translator
132
+ return Rx.combineLatest({
133
+ localTranslator = self._localTranslator:Observe();
134
+ sourceTranslator = self._sourceTranslator:Observe();
135
+ }):Pipe({
136
+ Rx.map(function(state)
137
+ if state.localTranslator then
138
+ return self:_doTranslation(state.localTranslator, mainState.translationKey, mainState.translationArgs)
139
+ elseif state.sourceTranslator then
140
+ return self:_doTranslation(state.sourceTranslator, mainState.translationKey, mainState.translationArgs)
141
+ else
142
+ return nil
143
+ end
144
+ end);
145
+ Rx.where(function(value)
146
+ return value ~= nil
147
+ end);
148
+ })
149
+ end)
150
+ })
151
+ end
152
+
153
+ --[=[
154
+ Formats the resulting entry by args.
155
+
156
+ :::tip
157
+ You should use [JSONTranslator.ObserveFormatByKey] instead of this to respond
158
+ to locale changing.
159
+ :::
160
+
161
+ @param translationKey string
162
+ @param args table?
163
+ @return Promise<string>
164
+ ]=]
165
+ function JSONTranslator:PromiseFormatByKey(translationKey, args)
166
+ assert(self ~= JSONTranslator, "Construct a new version of this class to use it")
167
+ assert(type(translationKey) == "string", "Key must be a string")
168
+
169
+ -- Always waits for full translator to be loaded since we only get one shot
170
+ return self:PromiseTranslator():Then(function(translator)
171
+ return self:_doTranslation(translator, translationKey, args)
172
+ end)
173
+ end
174
+
175
+ function JSONTranslator:PromiseTranslator()
176
+ return self._translatorService:PromiseTranslator()
177
+ end
178
+
179
+ function JSONTranslator:ObserveTranslator()
180
+ return self._translatorService:ObserveTranslator()
92
181
  end
93
182
 
94
183
  --[=[
@@ -97,11 +186,7 @@ end
97
186
  @return Observable<string>
98
187
  ]=]
99
188
  function JSONTranslator:ObserveLocaleId()
100
- return Rx.fromPromise(self._promiseTranslator):Pipe({
101
- Rx.switchMap(function(translator)
102
- return RxInstanceUtils.observeProperty(translator, "LocaleId")
103
- end)
104
- })
189
+ return self._translatorService:ObserveLocaleId()
105
190
  end
106
191
 
107
192
  --[=[
@@ -125,17 +210,15 @@ function JSONTranslator:SetEntryValue(translationKey, source, context, localeId,
125
210
  self._localizationTable:SetEntryValue(translationKey, source, context, localeId, text or source)
126
211
 
127
212
  if RunService:IsStudio() then
128
- self._localizationTable:SetEntryValue(translationKey, source, context,
129
- PseudoLocalize.getDefaultPseudoLocaleId(),
130
- PseudoLocalize.pseudoLocalize(text))
213
+ self._localizationTable:SetEntryValue(translationKey, source, context, PseudoLocalize.getDefaultPseudoLocaleId(), PseudoLocalize.pseudoLocalize(text))
131
214
  end
132
215
  end
133
216
 
134
- function JSONTranslator:ObserveTranslation(prefix, text, argData)
217
+ function JSONTranslator:ObserveTranslation(prefix, text, translationArgs)
135
218
  assert(type(prefix) == "string", "Bad text")
136
219
  assert(type(text) == "string", "Bad text")
137
220
 
138
- return self:ObserveFormatByKey(self:ToTranslationKey(prefix, text), argData)
221
+ return self:ObserveFormatByKey(self:ToTranslationKey(prefix, text), translationArgs)
139
222
  end
140
223
 
141
224
  function JSONTranslator:ToTranslationKey(prefix, text)
@@ -157,13 +240,7 @@ end
157
240
  @return string
158
241
  ]=]
159
242
  function JSONTranslator:GetLocaleId()
160
- if self._promiseTranslator:IsFulfilled() then
161
- local translator = self._promiseTranslator:Wait()
162
- return translator.LocaleId
163
- else
164
- warn("[JSONTranslator.GetLocaleId] - Translator is not loaded yet, returning english")
165
- return "en"
166
- end
243
+ return self._translatorService:GetLocaleId()
167
244
  end
168
245
 
169
246
  --[=[
@@ -180,188 +257,98 @@ end
180
257
  @return Promise
181
258
  ]=]
182
259
  function JSONTranslator:PromiseLoaded()
183
- return self._promiseTranslator
184
- end
185
-
186
- --[=[
187
- Makes the translator fall back to another translator if an entry cannot be found.
188
-
189
- Mostly just used for testing.
190
-
191
- @param translator JSONTranslator | Translator
192
- ]=]
193
- function JSONTranslator:FallbackTo(translator)
194
- assert(translator, "Bad translator")
195
- assert(translator.FormatByKey, "Bad translator")
196
-
197
- table.insert(self._fallbacks, translator)
260
+ return self:PromiseTranslator()
198
261
  end
199
262
 
200
263
  --[=[
201
- Formats the resulting entry by args.
264
+ Formats or errors if the cloud translations are not loaded.
202
265
 
203
266
  :::tip
204
267
  You should use [JSONTranslator.ObserveFormatByKey] instead of this to respond
205
268
  to locale changing.
206
269
  :::
207
270
 
208
- @param key string
271
+ @param translationKey string
209
272
  @param args table?
210
- @return Promise<string>
211
- ]=]
212
- function JSONTranslator:PromiseFormatByKey(key, args)
213
- assert(self ~= JSONTranslator, "Construct a new version of this class to use it")
214
- assert(type(key) == "string", "Key must be a string")
215
-
216
- return self._promiseTranslator:Then(function()
217
- return self:FormatByKey(key, args)
218
- end)
219
- end
220
-
221
- --[=[
222
- Observes the translated value
223
- @param key string
224
- @param argData table? -- May have observables (or convertable to observables) in it.
225
- @return Observable<string>
273
+ @return string
226
274
  ]=]
227
- function JSONTranslator:ObserveFormatByKey(key, argData)
275
+ function JSONTranslator:FormatByKey(translationKey, args)
228
276
  assert(self ~= JSONTranslator, "Construct a new version of this class to use it")
229
- assert(type(key) == "string", "Key must be a string")
230
-
231
- local argObservable
232
- if argData then
233
- local args = {}
234
- for argKey, value in pairs(argData) do
235
- args[argKey] = Blend.toPropertyObservable(value) or Rx.of(value)
236
- end
277
+ assert(type(translationKey) == "string", "Key must be a string")
237
278
 
238
- argObservable = Rx.combineLatest(args)
239
- else
240
- argObservable = nil
279
+ local translator = self._translatorService:GetTranslator()
280
+ if not translator then
281
+ error("Translator is not yet acquired yet")
241
282
  end
242
283
 
243
- return Observable.new(function(sub)
244
- local maid = Maid.new()
245
-
246
- maid:GivePromise(self._promiseTranslator):Then(function(translator)
247
- if argObservable then
248
- maid:GiveTask(Rx.combineLatest({
249
- localeId = RxInstanceUtils.observeProperty(translator, "LocaleId");
250
- args = argObservable;
251
- }):Subscribe(function(state)
252
- sub:Fire(self:FormatByKey(key, state.args))
253
- end))
254
- else
255
- maid:GiveTask(RxInstanceUtils.observeProperty(translator, "LocaleId"):Subscribe(function()
256
- sub:Fire(self:FormatByKey(key, nil))
257
- end))
258
- end
259
- end)
260
-
261
- return maid
262
- end)
284
+ return self:_doTranslation(translator, translationKey, args)
263
285
  end
264
286
 
265
- --[=[
266
- Formats or errors if the cloud translations are not loaded.
267
-
268
- :::tip
269
- You should use [JSONTranslator.ObserveFormatByKey] instead of this to respond
270
- to locale changing.
271
- :::
272
-
273
- @param key string
274
- @param args table?
275
- @return string
276
- ]=]
277
- function JSONTranslator:FormatByKey(key, args)
278
- assert(self ~= JSONTranslator, "Construct a new version of this class to use it")
279
- assert(type(key) == "string", "Key must be a string")
287
+ function JSONTranslator:_observeArgs(translationArgs)
288
+ if translationArgs == nil then
289
+ return Rx.of(nil)
290
+ end
280
291
 
281
- if not RunService:IsRunning() then
282
- return self:_formatByKeyTestMode(key, args)
292
+ local args = {}
293
+ for argKey, value in pairs(translationArgs) do
294
+ args[argKey] = Blend.toPropertyObservable(value) or Rx.of(value)
283
295
  end
284
296
 
285
- local clientTranslator = self:_getClientTranslatorOrError()
297
+ return Rx.combineLatest(args)
298
+ end
299
+
300
+ function JSONTranslator:_doTranslation(translator, translationKey, args)
301
+ assert(typeof(translator) == "Instance", "Bad translator")
302
+ assert(type(translationKey) == "string", "Bad translationKey")
286
303
 
287
- local result
304
+ local translation
288
305
  local ok, err = pcall(function()
289
- result = clientTranslator:FormatByKey(key, args)
306
+ translation = translator:FormatByKey(translationKey, args)
290
307
  end)
291
308
 
292
- if ok and not err then
293
- return result
309
+ if translation then
310
+ return translation
294
311
  end
295
312
 
296
313
  if err then
297
314
  warn(err)
298
315
  else
299
- warn("Failed to localize '" .. key .. "'")
316
+ warn("Failed to localize '" .. translationKey .. "'")
300
317
  end
301
318
 
302
- -- Fallback to English
303
- if clientTranslator.LocaleId ~= self._englishTranslator.LocaleId then
304
- -- Ignore results as we know this may error
319
+ -- Try the local translator next (not from cloud)
320
+ local localTranslator = self._localTranslator.Value
321
+ if localTranslator then
305
322
  ok, err = pcall(function()
306
- result = self._englishTranslator:FormatByKey(key, args)
323
+ translation = localTranslator:FormatByKey(translationKey, args)
307
324
  end)
308
325
 
309
- if ok and not err then
310
- return result
326
+ if translation then
327
+ return translation
311
328
  end
312
329
  end
313
330
 
314
- return key
315
- end
316
-
317
- function JSONTranslator:_getClientTranslatorOrError()
318
- assert(self._promiseTranslator, "ClientTranslator is not initialized")
319
-
320
- if self._promiseTranslator:IsFulfilled() then
321
- return assert(self._promiseTranslator:Wait(), "Failed to get translator")
322
- else
323
- error("Translator is not yet acquired yet")
324
- return nil
331
+ -- Try the source translator next (we're missing the locale id)
332
+ local sourceTranslator = self._sourceTranslator.Value
333
+ if sourceTranslator then
334
+ ok, err = pcall(function()
335
+ translation = sourceTranslator:FormatByKey(translationKey, args)
336
+ end)
325
337
  end
326
- end
327
-
328
- function JSONTranslator:_formatByKeyTestMode(key, args)
329
- -- Can't read LocalizationService.ForcePlayModeRobloxLocaleId :(
330
- local translator = self._localizationTable:GetTranslator("en")
331
- local result
332
- local ok, err = pcall(function()
333
- result = translator:FormatByKey(key, args)
334
- end)
335
338
 
336
339
  if ok and not err then
337
- return result
338
- end
339
-
340
- for _, fallback in pairs(self._fallbacks) do
341
- local value = fallback:FormatByKey(key, args)
342
- if value then
343
- return value
344
- end
340
+ return translation
345
341
  end
346
342
 
347
- if err then
348
- warn(err)
349
- else
350
- warn("[JSONTranslator._formatByKeyTestMode] - Failed to localize '" .. key .. "'")
351
- end
352
-
353
- return key
343
+ return translationKey
354
344
  end
355
345
 
356
346
  --[=[
357
347
  Cleans up the translator and deletes the localization table if it exists.
348
+ Should be called by [ServiceBag]
358
349
  ]=]
359
350
  function JSONTranslator:Destroy()
360
- self._localizationTable:Destroy()
361
- self._localizationTable = nil
362
- self._englishTranslator = nil
363
- self._promiseTranslator = nil
364
-
351
+ self._maid:DoCleaning()
365
352
  setmetatable(self, nil)
366
353
  end
367
354
 
@@ -0,0 +1,212 @@
1
+ --[=[
2
+ Handles selecting the right locale/translator for Studio, and Roblox games.
3
+
4
+ @class TranslatorService
5
+ ]=]
6
+
7
+ local require = require(script.Parent.loader).load(script)
8
+
9
+ local Players = game:GetService("Players")
10
+ local RunService = game:GetService("RunService")
11
+ local LocalizationService = game:GetService("LocalizationService")
12
+
13
+ local RxInstanceUtils = require("RxInstanceUtils")
14
+ local Rx = require("Rx")
15
+ local LocalizationServiceUtils = require("LocalizationServiceUtils")
16
+ local ValueObject = require("ValueObject")
17
+ local Maid = require("Maid")
18
+ local Promise = require("Promise")
19
+
20
+ local TranslatorService = {}
21
+ TranslatorService.ServiceName = "TranslatorService"
22
+
23
+ function TranslatorService:Init(serviceBag)
24
+ assert(not self._serviceBag, "Already initialized")
25
+ self._serviceBag = assert(serviceBag, "No serviceBag")
26
+ self._maid = Maid.new()
27
+
28
+ self._translator = self._maid:Add(ValueObject.new(nil))
29
+ self._translator:Mount(self:_observeTranslatorImpl())
30
+ end
31
+
32
+ function TranslatorService:GetLocalizationTable()
33
+ if self._localizationTable then
34
+ return self._localizationTable
35
+ end
36
+
37
+ local localizationTableName = self:_getLocalizationTableName()
38
+ local localizationTable = LocalizationService:FindFirstChild(localizationTableName)
39
+
40
+ if not localizationTable then
41
+ localizationTable = Instance.new("LocalizationTable")
42
+ localizationTable.Name = localizationTableName
43
+ localizationTable.Parent = LocalizationService
44
+ end
45
+
46
+ self._localizationTable = localizationTable
47
+ return localizationTable
48
+ end
49
+
50
+ function TranslatorService:_getLocalizationTableName()
51
+ if RunService:IsServer() then
52
+ return "GeneratedJSONTable_Server"
53
+ else
54
+ return "GeneratedJSONTable_Client"
55
+ end
56
+ end
57
+
58
+ --[=[
59
+ Observes Roblox translator
60
+
61
+ @return Observable<Translator>
62
+ ]=]
63
+ function TranslatorService:ObserveTranslator()
64
+ return self._translator:Observe()
65
+ end
66
+
67
+ --[=[
68
+ Promises the Roblox translator
69
+
70
+ @return Observable<Translator>
71
+ ]=]
72
+ function TranslatorService:PromiseTranslator()
73
+ local found = self._translator.Value
74
+ if found then
75
+ return Promise.resolved(found)
76
+ end
77
+
78
+ if self._pendingTranslatorPromise then
79
+ return self._pendingTranslatorPromise
80
+ end
81
+
82
+ local maid = Maid.new()
83
+ local promise = maid:Add(Promise.new())
84
+
85
+ self._maid._pendingTranslatorMaid = maid
86
+ self._pendingTranslatorPromise = promise
87
+
88
+ maid:GiveTask(function()
89
+ if self._maid._pendingTranslatorMaid == maid then
90
+ self._maid._pendingTranslatorMaid = nil
91
+ end
92
+
93
+ if self._pendingTranslatorPromise == promise then
94
+ self._pendingTranslatorPromise = nil
95
+ end
96
+ end)
97
+
98
+ maid:GiveTask(self._translator:Observe():Subscribe(function(translator)
99
+ if translator then
100
+ promise:Resolve(translator)
101
+ end
102
+ end))
103
+
104
+ return promise
105
+ end
106
+
107
+ --[=[
108
+ Gets the current translator to use
109
+
110
+ @return Translator?
111
+ ]=]
112
+ function TranslatorService:GetTranslator()
113
+ return self._translator.Value
114
+ end
115
+
116
+ --[=[
117
+ Observes the current locale id for this translator.
118
+
119
+ @return Observable<string>
120
+ ]=]
121
+ function TranslatorService:ObserveLocaleId()
122
+ return self._translator:Observe():Pipe({
123
+ Rx.switchMap(function(translator)
124
+ if translator then
125
+ return RxInstanceUtils.observeProperty(translator, "LocaleId")
126
+ else
127
+ -- Fallback
128
+ return self:_observeLoadedPlayer():Pipe({
129
+ Rx.switchMap(function(player)
130
+ if player then
131
+ return RxInstanceUtils.observeProperty(player, "LocaleId")
132
+ else
133
+ return RxInstanceUtils.observeProperty(LocalizationService, "RobloxLocaleId")
134
+ end
135
+ end)
136
+ })
137
+ end
138
+ end);
139
+ Rx.distinct();
140
+ })
141
+ end
142
+
143
+ --[=[
144
+ Gets the localeId to use
145
+
146
+ @return string
147
+ ]=]
148
+ function TranslatorService:GetLocaleId()
149
+ local found = self._translator.Value
150
+ if found then
151
+ return found.LocaleId
152
+ end
153
+
154
+ -- Fallback
155
+ local player = Players.LocalPlayer
156
+ if player and player.LocaleId ~= "" then
157
+ return player.LocaleId
158
+ else
159
+ return LocalizationService.RobloxLocaleId
160
+ end
161
+ end
162
+
163
+ function TranslatorService:_observeTranslatorImpl()
164
+ return self:_observeLoadedPlayer():Pipe({
165
+ Rx.switchMap(function(loadedPlayer)
166
+ if loadedPlayer then
167
+ return Rx.fromPromise(LocalizationServiceUtils.promisePlayerTranslator(loadedPlayer))
168
+ end
169
+
170
+ return RxInstanceUtils.observeProperty(LocalizationService, "RobloxLocaleId"):Pipe({
171
+ Rx.switchMap(function(localeId)
172
+ -- This can actually take a while (20-30 seconds)
173
+ return Rx.fromPromise(LocalizationServiceUtils.promiseTranslatorForLocale(localeId))
174
+ end)
175
+ })
176
+ end);
177
+ })
178
+ end
179
+
180
+ function TranslatorService:_observeLoadedPlayer()
181
+ if self._loadedPlayerObservable then
182
+ return self._loadedPlayerObservable
183
+ end
184
+
185
+ self._loadedPlayerObservable = RxInstanceUtils.observeProperty(Players, "LocalPlayer"):Pipe({
186
+ Rx.switchMap(function(player)
187
+ if not player then
188
+ return Rx.of(nil)
189
+ end
190
+
191
+ return RxInstanceUtils.observeProperty(player, "LocaleId"):Pipe({
192
+ Rx.map(function(localeId)
193
+ if localeId == "" then
194
+ return nil
195
+ else
196
+ return player
197
+ end
198
+ end);
199
+ })
200
+ end);
201
+ Rx.distinct();
202
+ Rx.cache();
203
+ })
204
+
205
+ return self._loadedPlayerObservable
206
+ end
207
+
208
+ function TranslatorService:Destroy()
209
+ self._maid:DoCleaning()
210
+ end
211
+
212
+ return TranslatorService
@@ -8,53 +8,64 @@ local LocalizationService = game:GetService("LocalizationService")
8
8
  local RunService = game:GetService("RunService")
9
9
 
10
10
  local Promise = require("Promise")
11
+ local PromiseMaidUtils = require("PromiseMaidUtils")
12
+
13
+ local TIMEOUT = 20
14
+ if RunService:IsStudio() then
15
+ TIMEOUT = 0.5
16
+ end
17
+
11
18
  local ERROR_PUBLISH_REQUIRED = "Publishing the game is required to use GetTranslatorForPlayerAsync API."
19
+ local ERROR_TIMEOUT = string.format("GetTranslatorForPlayerAsync is still pending after %f, using local table", TIMEOUT)
12
20
 
13
21
  local LocalizationServiceUtils = {}
14
22
 
15
- function LocalizationServiceUtils.promiseTranslator(player)
16
- local asyncTranslatorPromise = Promise.spawn(function(resolve, reject)
23
+ function LocalizationServiceUtils.promiseTranslatorForLocale(localeId)
24
+ return Promise.spawn(function(resolve, reject)
17
25
  local translator = nil
18
26
  local ok, err = pcall(function()
19
- translator = LocalizationService:GetTranslatorForPlayerAsync(player)
27
+ translator = LocalizationService:GetTranslatorForLocaleAsync(localeId)
20
28
  end)
21
29
 
22
30
  if not ok then
23
- reject(err or "Failed to GetTranslatorForPlayerAsync")
24
- return
31
+ return reject(err or "Failed to GetTranslatorForLocaleAsync")
25
32
  end
26
33
 
27
- if translator then
28
- assert(typeof(translator) == "Instance", "Bad translator")
29
- resolve(translator)
30
- return
34
+ if typeof(translator) ~= "Instance" then
35
+ return reject("Translator was not returned")
31
36
  end
32
37
 
33
- reject("Translator was not returned")
34
- return
38
+ return resolve(translator)
35
39
  end)
40
+ end
36
41
 
37
- -- Give longer in non-studio mode
38
- local timeout = 20
39
- if RunService:IsStudio() then
40
- timeout = 0.5
41
- end
42
+ function LocalizationServiceUtils.promisePlayerTranslator(player)
43
+ local promiseTranslator = Promise.spawn(function(resolve, reject)
44
+ local translator = nil
45
+ local ok, err = pcall(function()
46
+ translator = LocalizationService:GetTranslatorForPlayerAsync(player)
47
+ end)
48
+
49
+ if not ok then
50
+ return reject(err or "Failed to GetTranslatorForPlayerAsync")
51
+ end
42
52
 
43
- local rejectedCauseOfTimeout = false
44
- task.delay(timeout, function()
45
- if not asyncTranslatorPromise:IsPending() then
46
- return
53
+ if typeof(translator) ~= "Instance" then
54
+ return reject("Translator was not returned")
47
55
  end
48
56
 
49
- rejectedCauseOfTimeout = true
50
- asyncTranslatorPromise:Reject(
51
- ("GetTranslatorForPlayerAsync is still pending after %f, using local table")
52
- :format(timeout))
57
+ return resolve(translator)
53
58
  end)
54
59
 
55
- return asyncTranslatorPromise:Catch(function(err)
56
- if err ~= ERROR_PUBLISH_REQUIRED and not rejectedCauseOfTimeout then
57
- warn(("[LocalizationServiceUtils.promiseTranslator] - %s"):format(tostring(err)))
60
+ PromiseMaidUtils.whilePromise(promiseTranslator, function(maid)
61
+ maid:GiveTask(task.delay(TIMEOUT, function()
62
+ promiseTranslator:Reject(ERROR_TIMEOUT)
63
+ end))
64
+ end)
65
+
66
+ return promiseTranslator:Catch(function(err)
67
+ if err ~= ERROR_PUBLISH_REQUIRED and error ~= ERROR_TIMEOUT then
68
+ warn(string.format("[LocalizationServiceUtils.promisePlayerTranslator] - %s", tostring(err)))
58
69
  end
59
70
 
60
71
  -- Fallback to just local stuff
@@ -63,5 +74,4 @@ function LocalizationServiceUtils.promiseTranslator(player)
63
74
  end)
64
75
  end
65
76
 
66
-
67
77
  return LocalizationServiceUtils
@@ -1,174 +0,0 @@
1
- --[=[
2
- Utility to build a localization table from json, intended to be used with rojo. Can also handle Rojo json
3
- objects turned into tables!
4
-
5
- @class JsonToLocalizationTable
6
- ]=]
7
-
8
- local LocalizationService = game:GetService("LocalizationService")
9
- local HttpService = game:GetService("HttpService")
10
- local RunService = game:GetService("RunService")
11
-
12
- local JsonToLocalizationTable = {}
13
-
14
- local LOCALIZATION_TABLE_NAME_CLIENT = "GeneratedJSONTable_Client"
15
- local LOCALIZATION_TABLE_NAME_SERVER = "GeneratedJSONTable_Server"
16
-
17
- --[[
18
- Recursively iterates through the object to construct strings and add it to the localization table
19
-
20
- @param localizationTable LocalizationTable
21
- @param localeId string -- The localizationid to add
22
- @param baseKey string -- the key to add
23
- @param object any -- The value to iterate over
24
- ]]
25
- local function recurseAdd(localizationTable, localeId, baseKey, object, tableName)
26
- if baseKey ~= "" then
27
- baseKey = baseKey .. "."
28
- end
29
-
30
- for index, value in pairs(object) do
31
- local key = baseKey .. index
32
- if type(value) == "table" then
33
- recurseAdd(localizationTable, localeId, key, value, tableName)
34
- elseif type(value) == "string" then
35
- local source = ""
36
-
37
- -- Guarantee the context is unique. This is important because Roblox will not
38
- -- allow something with the same source without a differing context value.
39
- local context = tableName .. "." .. key
40
-
41
- if localeId == "en" then
42
- source = value
43
- end
44
-
45
- localizationTable:SetEntryValue(key, source, context, localeId, value)
46
- else
47
- error("Bad type for value in '" .. key .. "'.")
48
- end
49
- end
50
- end
51
-
52
- --[=[
53
- Extracts the locale from the name
54
-
55
- @param name string -- The name to parse
56
- @return string -- The locale
57
- ]=]
58
- function JsonToLocalizationTable.localeFromName(name)
59
- if name:sub(-5) == ".json" then
60
- return name:sub(1, #name-5)
61
- else
62
- return name
63
- end
64
- end
65
-
66
- --[=[
67
- Gets or creates the global localization table. If the game isn't running (i.e. test mode), then
68
- we'll just not parent it.
69
-
70
- @return string -- The locale
71
- ]=]
72
- function JsonToLocalizationTable.getOrCreateLocalizationTable()
73
- local localizationTableName
74
- if RunService:IsServer() then
75
- localizationTableName = LOCALIZATION_TABLE_NAME_SERVER
76
- else
77
- localizationTableName = LOCALIZATION_TABLE_NAME_CLIENT
78
- end
79
-
80
- local localizationTable = LocalizationService:FindFirstChild(localizationTableName)
81
-
82
- if not localizationTable then
83
- localizationTable = Instance.new("LocalizationTable")
84
- localizationTable.Name = localizationTableName
85
-
86
- if RunService:IsRunning() then
87
- localizationTable.Parent = LocalizationService
88
- end
89
- end
90
-
91
- return localizationTable
92
- end
93
-
94
- --[=[
95
- Loads a folder into a localization table.
96
-
97
- @param tableName string -- Used for source
98
- @param folder Folder -- A Roblox folder with StringValues containing JSON, named with the localization in mind
99
- ]=]
100
- function JsonToLocalizationTable.loadFolder(tableName, folder)
101
- assert(type(tableName) == "string", "Bad tableName")
102
-
103
- local localizationTable = JsonToLocalizationTable.getOrCreateLocalizationTable()
104
-
105
- for _, item in pairs(folder:GetDescendants()) do
106
- if item:IsA("StringValue") then
107
- local localeId = JsonToLocalizationTable.localeFromName(item.Name)
108
- JsonToLocalizationTable.addJsonToTable(localizationTable, localeId, item.Value, tableName)
109
- elseif item:IsA("ModuleScript") then
110
- local localeId = JsonToLocalizationTable.localeFromName(item.Name)
111
- recurseAdd(localizationTable, localeId, "", require(item), tableName)
112
- end
113
- end
114
- return localizationTable
115
- end
116
-
117
- --[=[
118
- Extracts the locale from the folder, or a locale and table.
119
-
120
- @param tableName string -- Used for source
121
- @param first Instance | string
122
- @param second table?
123
- @return LocalizationTable
124
- ]=]
125
- function JsonToLocalizationTable.toLocalizationTable(tableName, first, second)
126
- assert(type(tableName) == "string", "Bad tableName")
127
-
128
- if typeof(first) == "Instance" then
129
- local result = JsonToLocalizationTable.loadFolder(tableName, first)
130
- -- result.Name = ("JSONTable_%s"):format(first.Name)
131
- return result
132
- elseif type(first) == "string" and type(second) == "table" then
133
- local result = JsonToLocalizationTable.loadTable(tableName, first, second)
134
- return result
135
- else
136
- error("Bad args")
137
- end
138
- end
139
-
140
- --[=[
141
- Extracts the locale from the name
142
-
143
- @param tableName string -- Used for source
144
- @param localeId string -- the defaultlocaleId
145
- @param dataTable table -- Data table to load from
146
- @return LocalizationTable
147
- ]=]
148
- function JsonToLocalizationTable.loadTable(tableName, localeId, dataTable)
149
- assert(type(tableName) == "string", "Bad tableName")
150
-
151
- local localizationTable = JsonToLocalizationTable.getOrCreateLocalizationTable()
152
-
153
- recurseAdd(localizationTable, localeId, "", dataTable, tableName)
154
-
155
- return localizationTable
156
- end
157
-
158
- --[=[
159
- Adds json to a localization table
160
-
161
- @param localizationTable LocalizationTable -- The localization table to add to
162
- @param localeId string -- The localeId to use
163
- @param json string -- The json to add with
164
- @param tableName string -- Used for source
165
- ]=]
166
- function JsonToLocalizationTable.addJsonToTable(localizationTable, localeId, json, tableName)
167
- assert(type(tableName) == "string", "Bad tableName")
168
-
169
- local decodedTable = HttpService:JSONDecode(json)
170
- recurseAdd(localizationTable, localeId, "", decodedTable, tableName)
171
- end
172
-
173
- return JsonToLocalizationTable
174
-