diff --git a/webapp/src/Cache.ts b/webapp/src/Cache.ts index 9ca9a511..07d0e840 100644 --- a/webapp/src/Cache.ts +++ b/webapp/src/Cache.ts @@ -2,6 +2,7 @@ import { LanguageNotSupported } from '@/errors'; import { LyraProjectConfig } from '@/utils/lyraConfig'; +import { MessageMap } from '@/utils/adapters'; import { ProjectStore } from '@/store/ProjectStore'; import { RepoGit } from '@/RepoGit'; import { ServerConfig } from '@/utils/serverConfig'; @@ -9,7 +10,10 @@ import { Store } from '@/store/Store'; import YamlTranslationAdapter from '@/utils/adapters/YamlTranslationAdapter'; export class Cache { - public static async getLanguage(projectName: string, lang: string) { + public static async getLanguage( + projectName: string, + lang: string, + ): Promise { const serverProjectConfig = await ServerConfig.getProjectConfig(projectName); const repoGit = await RepoGit.getRepoGit(serverProjectConfig); diff --git a/webapp/src/RepoGit.ts b/webapp/src/RepoGit.ts index 4c00d79e..3aa590ad 100644 --- a/webapp/src/RepoGit.ts +++ b/webapp/src/RepoGit.ts @@ -12,6 +12,7 @@ import { ServerProjectConfig } from '@/utils/serverConfig'; import { SimpleGitWrapper } from '@/utils/git/SimpleGitWrapper'; import { unflattenObject } from '@/utils/unflattenObject'; import { debug, info, warn } from '@/utils/log'; +import { groupByFilename, TranslationMap } from '@/utils/adapters'; import { WriteLanguageFileError, WriteLanguageFileErrors } from '@/errors'; export class RepoGit { @@ -143,27 +144,26 @@ export class RepoGit { } private async writeLangFiles( - languages: Record>, + languages: TranslationMap, translationsPath: string, ): Promise { const paths: string[] = []; + const translateFilenameMap = groupByFilename(languages); const result = await Promise.allSettled( - Object.keys(languages).map(async (lang) => { - const yamlPath = path.join( - translationsPath, - // TODO: what if language file were yaml not yml? - `${lang}.yml`, - ); - const yamlOutput = stringify(unflattenObject(languages[lang]), { - doubleQuotedAsJSON: true, - singleQuote: true, + Object.values(translateFilenameMap).map((filenames) => { + Object.entries(filenames).forEach(async ([filename, messages]) => { + const yamlPath = path.join(translationsPath, filename); + const yamlOutput = stringify(unflattenObject(messages), { + doubleQuotedAsJSON: true, + singleQuote: true, + }); + try { + await fsp.writeFile(yamlPath, yamlOutput); + } catch (e) { + throw new WriteLanguageFileError(yamlPath, e); + } + paths.push(yamlPath); }); - try { - await fsp.writeFile(yamlPath, yamlOutput); - } catch (e) { - throw new WriteLanguageFileError(yamlPath, e); - } - paths.push(yamlPath); }), ); if (result.some((r) => r.status === 'rejected')) { diff --git a/webapp/src/store/ProjectStore.spec.ts b/webapp/src/store/ProjectStore.spec.ts index c18c12ee..27fff4a3 100644 --- a/webapp/src/store/ProjectStore.spec.ts +++ b/webapp/src/store/ProjectStore.spec.ts @@ -20,13 +20,13 @@ describe('ProjectStore', () => { getTranslations: async () => ({ de: { 'greeting.headline': { - sourceFile: '', + sourceFile: 'de-with_extra_text.yaml', text: 'Hallo', }, }, sv: { 'greeting.headline': { - sourceFile: '', + sourceFile: 'sv.yaml', text: 'Hej', }, }, @@ -35,7 +35,10 @@ describe('ProjectStore', () => { const actual = await projectStore.getTranslations('de'); expect(actual).toEqual({ - 'greeting.headline': 'Hallo', + 'greeting.headline': { + sourceFile: 'de-with_extra_text.yaml', + text: 'Hallo', + }, }); }); @@ -54,24 +57,22 @@ describe('ProjectStore', () => { getTranslations: async () => ({ de: { 'greeting.headline': { - sourceFile: '', + sourceFile: 'anything', text: 'Hallo', }, }, }), }); - const before = await projectStore.getTranslations('de'); + const beforeTranslate = await projectStore.getTranslations('de'); + const before = beforeTranslate['greeting.headline'].text; await projectStore.updateTranslation('de', 'greeting.headline', 'Hallo!'); - const after = await projectStore.getTranslations('de'); + const afterTranslate = await projectStore.getTranslations('de'); + const after = afterTranslate['greeting.headline'].text; - expect(before).toEqual({ - 'greeting.headline': 'Hallo', - }); + expect(before).toEqual('Hallo'); - expect(after).toEqual({ - 'greeting.headline': 'Hallo!', - }); + expect(after).toEqual('Hallo!'); }); it('can update translations before getTranslations()', async () => { @@ -79,7 +80,7 @@ describe('ProjectStore', () => { getTranslations: async () => ({ de: { 'greeting.headline': { - sourceFile: '', + sourceFile: 'anything', text: 'Hallo', }, }, @@ -90,7 +91,7 @@ describe('ProjectStore', () => { const actual = await projectStore.getTranslations('de'); expect(actual).toEqual({ - 'greeting.headline': 'Hallo!', + 'greeting.headline': { sourceFile: 'anything', text: 'Hallo!' }, }); }); @@ -147,10 +148,16 @@ describe('ProjectStore', () => { const languages = await projectStore.getLanguageData(); expect(languages).toEqual({ de: { - 'greeting.headline': 'Hallo', + 'greeting.headline': { + sourceFile: '', + text: 'Hallo', + }, }, sv: { - 'greeting.headline': 'Hej', + 'greeting.headline': { + sourceFile: '', + text: 'Hej', + }, }, }); }); diff --git a/webapp/src/store/ProjectStore.ts b/webapp/src/store/ProjectStore.ts index a353b59a..1dbf78f4 100644 --- a/webapp/src/store/ProjectStore.ts +++ b/webapp/src/store/ProjectStore.ts @@ -1,4 +1,8 @@ -import { ITranslationAdapter, TranslationMap } from '@/utils/adapters'; +import { + ITranslationAdapter, + MessageMap, + TranslationMap, +} from '@/utils/adapters'; import { LanguageNotFound, MessageNotFound } from '@/errors'; type StoreData = { @@ -17,10 +21,10 @@ export class ProjectStore { this.translationAdapter = translationAdapter; } - async getLanguageData(): Promise>> { + async getLanguageData(): Promise { await this.initIfNecessary(); - const output: Record> = {}; + const output: TranslationMap = {}; for await (const lang of Object.keys(this.data.languages)) { output[lang] = await this.getTranslations(lang); } @@ -28,7 +32,7 @@ export class ProjectStore { return output; } - async getTranslations(lang: string): Promise> { + async getTranslations(lang: string): Promise { await this.initIfNecessary(); const language = this.data.languages[lang]; @@ -36,9 +40,9 @@ export class ProjectStore { throw new LanguageNotFound(lang); } - const output: Record = {}; + const output: MessageMap = {}; Object.entries(language).forEach(([key, value]) => { - output[key] = value.text; + output[key] = value; }); return output; diff --git a/webapp/src/utils/adapters/index.ts b/webapp/src/utils/adapters/index.ts index c65c795e..b075d2e4 100644 --- a/webapp/src/utils/adapters/index.ts +++ b/webapp/src/utils/adapters/index.ts @@ -9,15 +9,19 @@ export type MessageData = { }[]; }; +export type MessageTranslation = { + sourceFile: string; + text: string; +}; + +export type MessageMap = Record< + string, // msg id + MessageTranslation +>; + export type TranslationMap = Record< - string, - Record< - string, - { - sourceFile: string; - text: string; - } - > + string, // lang + MessageMap >; export interface IMessageAdapter { @@ -27,3 +31,51 @@ export interface IMessageAdapter { export interface ITranslationAdapter { getTranslations(): Promise; } + +/** convert { [id]: { file, texts } } to { [id]: text } */ +export function dehydrateMessageMap( + translations: MessageMap, +): Record { + const output: Record = {}; + Object.entries(translations).forEach(([id, { text }]) => { + output[id] = text; + }); + return output; +} + +type TranslationFilenameMap = Record< + string, // lang + Record< + string, // filename + Record< + string, // msg Id + string // text + > + > +>; + +/** convert TranslateMap to { [lang]: { [filename]: {[msgId]: text} } */ +export function groupByFilename( + translationMap: TranslationMap, +): TranslationFilenameMap { + const output: TranslationFilenameMap = {}; + Object.entries(translationMap).forEach(([lang, messageMap]) => { + Object.entries(messageMap).forEach(([msgId, { text, sourceFile }]) => { + if (!output[lang]) { + output[lang] = {}; + } + if (!output[lang][sourceFile]) { + output[lang][sourceFile] = {}; + } + const shortId = getShortId(msgId, sourceFile); + output[lang][sourceFile][shortId] = text; + }); + }); + return output; +} + +function getShortId(msgId: string, sourceFile: string): string { + const sourceFileDotCount = sourceFile.replace('/', '.').split('.').length - 1; + const msgIdArray = msgId.split('.').splice(sourceFileDotCount); + return msgIdArray.join('.'); +}