Skip to content

Commit

Permalink
analyzeFile
Browse files Browse the repository at this point in the history
  • Loading branch information
eddow committed May 13, 2024
1 parent f1deaf2 commit 719c82f
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 33 deletions.
9 changes: 9 additions & 0 deletions docs/db.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ This allows:
- All the translations to simply be gathered under a file under source control (backup-able)
- The development activities (adding/removing/removing/rezoning a key) to be made and applied on commit/merge, and the "translation" (text-change) activities to still be available through the UI in real time
#### Recovering a file to export to a database
An `FileDB.analyze` function is exposed who takes the string to analyze and 2/3 callbacks
- `onKey` called when a new key is discovered
- `onText` called when a translation is discovered
- `endKey?` called when the key is finished
For each key, the callback calls will be `onKey - onText* - endKey` for each key
#### File format
The serialization file-format is specific for regexp-ability _and_ human interactions; grouping is done by indentation (made with tabulations - `\t`).
Expand Down
58 changes: 45 additions & 13 deletions src/db/fileDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,42 @@ export default class FileDB<KeyInfos extends {}, TextInfos extends {}> extends M
static deserialize<KeyInfos extends {} = {}, TextInfos extends {} = {}>(data: string) {
if (!data.endsWith('\n')) data += '\n'
const dictionary: MemDBDictionary<KeyInfos, TextInfos> = {}
FileDB.analyze<KeyInfos, TextInfos>(
data,
(key, zone, infos) => {
dictionary[key] = {
...(infos && { '.keyInfos': infos }),
'.zone': zone,
'.textInfos': {}
} as MemDBDictionaryEntry<KeyInfos, TextInfos>
},
(key, locale, text, infos) => {
if (infos) dictionary[key]['.textInfos']![locale] = infos
if (text !== undefined) dictionary[key][locale] = text
},
(key) => {
if (Object.values(dictionary[key]['.textInfos']!).length === 0)
delete dictionary[key]['.textInfos']
}
)
return dictionary
}

/**
* Analyze a file content. A key-context can be used in the scope as the callback calls will always be:
* `onKey - onText* - endKey` for each key
* @param data The textual data to analyze
* @param onKey The callback to enter a key
* @param onText The callback to specify a translation
* @param endKey The callback when a key is finished
*/
static analyze<KeyInfos extends {} = {}, TextInfos extends {} = {}>(
data: string,
onKey: (key: TextKey, zone: Zone, infos: KeyInfos) => void,
onText: (key: TextKey, locale: Locale, text: Translation, infos: TextInfos) => void,
endKey?: (key: TextKey) => void
) {
if (!data.endsWith('\n')) data += '\n'
data = data.replace(/\n/g, '\u0000') // Only way to make regexp treat '\n' as a regular character
const rex = {
key: /([^\t\{:]+)(\{.*?\})?:([^\u0000]*)\u0000/g,
Expand All @@ -135,29 +171,25 @@ export default class FileDB<KeyInfos extends {}, TextInfos extends {}> extends M
if (keyFetch.index > lastIndex) throw parseError(data, lastIndex, keyFetch.index)
const key = keyFetch[1],
zone = keyFetch[3] as Zone
let keyInfos: any,
textInfos: Record<Locale, any> = {}
let keyInfos: any
if (keyFetch[2]) keyInfos = parse(keyFetch[2].replace(/\u0000/g, '\n'))
const entry: MemDBDictionaryEntry<KeyInfos, TextInfos> = {
'.zone': zone,
...(keyInfos && { '.keyInfos': keyInfos })
}
onKey(key, zone, keyInfos)
let localeFetch: RegExpExecArray | null
rex.locale.lastIndex = lastIndex = rex.key.lastIndex
while ((localeFetch = rex.locale.exec(data))) {
if (localeFetch.index > lastIndex) break
lastIndex = rex.locale.lastIndex
if (localeFetch[3])
entry[localeFetch[1] as Locale] = localeFetch[3].replace(/\u0000\t\t/g, '\n')
if (localeFetch[2])
textInfos[localeFetch[1] as Locale] = parse(localeFetch[2].replace(/\u0000/g, '\n'))
onText(
key,
localeFetch[1] as Locale,
localeFetch[3] && localeFetch[3].replace(/\u0000\t\t/g, '\n'),
localeFetch[2] && parse(localeFetch[2].replace(/\u0000/g, '\n'))
)
}
endKey?.(key)
rex.key.lastIndex = lastIndex
if (Object.keys(textInfos).length) entry['.textInfos'] = textInfos
dictionary[key] = entry
}
if (rex.key.lastIndex > 0 || !rex.key.test(data)) throw parseError(data, rex.key.lastIndex)
return dictionary
}

//#endregion
Expand Down
11 changes: 2 additions & 9 deletions src/server/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,10 @@ export default class InteractiveServer<
keyInfos?: Partial<KeyInfos>,
textInfos?: Partial<TextInfos>
): Promise<void> {
const keyModified = await this.db.key(key, zone, keyInfos)
if (keyModified) {
for (const [locale, text] of Object.entries({
...(await this.db.get(key)),
...translations
}))
this.modifications.push([key, locale, zone, text])
}
await this.db.key(key, zone, keyInfos)
await Promise.all(
Object.entries(translations).map(async ([locale, text]) => {
if ((await this.db.modify(key, locale, text, textInfos)) !== false && !keyModified)
if ((await this.db.modify(key, locale, text, textInfos)) !== false)
this.modifications.push([key, locale, zone, text])
})
)
Expand Down
11 changes: 0 additions & 11 deletions test/dynamic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,4 @@ describe('Dynamic functionality', () => {
modifications = []
expect(T.cmd.remove()).toBe('[cmd.remove]')
})

test('zone modification', async () => {
await server.key('cmd.modify', '', { fr: 'Modifie' })
await server.propagate()
// The text has not changed but the zone did
expect(modifications).toEqual([{ 'cmd.modify': 'Modify' }])
modifications = []
await server.key('cmd.modify', '', { fr: 'Modifier' })
await server.propagate()
expect(modifications).toEqual([])
})
})

0 comments on commit 719c82f

Please sign in to comment.