@messagevisor/catalog 0.7.0 → 0.8.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/dist/assets/index-4rkVIXGk.js +73 -0
- package/dist/index.html +1 -1
- package/lib/node/index.d.ts +53 -0
- package/lib/node/index.js +639 -100
- package/lib/node/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/lists/EntityList.tsx +207 -63
- package/src/node/index.spec.ts +104 -1
- package/src/node/index.ts +710 -51
- package/dist/assets/index-Cx9rnVIG.js +0 -73
package/src/node/index.ts
CHANGED
|
@@ -341,6 +341,8 @@ export interface CatalogExportOptions {
|
|
|
341
341
|
devEditors?: CatalogDevEditor[];
|
|
342
342
|
withTranslationSearch?: boolean;
|
|
343
343
|
withDuplicates?: boolean;
|
|
344
|
+
devSession?: CatalogDevSession;
|
|
345
|
+
preserveAssets?: boolean;
|
|
344
346
|
}
|
|
345
347
|
|
|
346
348
|
export interface CatalogServeOptions {
|
|
@@ -371,6 +373,22 @@ interface CatalogBuildContext {
|
|
|
371
373
|
writer: CatalogJsonWriter;
|
|
372
374
|
}
|
|
373
375
|
|
|
376
|
+
interface CatalogDevSession {
|
|
377
|
+
outputDirectoryPath: string;
|
|
378
|
+
devEditors: CatalogDevEditor[];
|
|
379
|
+
historyIndex: CatalogHistoryIndex;
|
|
380
|
+
links: ReturnType<typeof getRepoLinks>;
|
|
381
|
+
repositoryRootDirectoryPath: string;
|
|
382
|
+
repositorySourceRootDirectoryPath: string;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
interface CatalogDevRebuildRequest {
|
|
386
|
+
kind: "full" | "set" | "message";
|
|
387
|
+
reason: string;
|
|
388
|
+
set?: string;
|
|
389
|
+
messageKeys?: string[];
|
|
390
|
+
}
|
|
391
|
+
|
|
374
392
|
interface SourceFileInfo {
|
|
375
393
|
sourcePath: string;
|
|
376
394
|
absolutePath: string;
|
|
@@ -1977,6 +1995,455 @@ async function copyCatalogAssets(outputDirectoryPath: string) {
|
|
|
1977
1995
|
await fs.promises.cp(distPath, outputDirectoryPath, { recursive: true });
|
|
1978
1996
|
}
|
|
1979
1997
|
|
|
1998
|
+
async function createCatalogDevSession(
|
|
1999
|
+
rootDirectoryPath: string,
|
|
2000
|
+
projectConfig: any,
|
|
2001
|
+
options: { outDir?: string; devEditors?: CatalogDevEditor[] } = {},
|
|
2002
|
+
): Promise<CatalogDevSession> {
|
|
2003
|
+
const outputDirectoryPath = options.outDir
|
|
2004
|
+
? path.resolve(rootDirectoryPath, options.outDir)
|
|
2005
|
+
: projectConfig.catalogDirectoryPath;
|
|
2006
|
+
|
|
2007
|
+
return {
|
|
2008
|
+
outputDirectoryPath,
|
|
2009
|
+
devEditors: options.devEditors || detectDevEditors(),
|
|
2010
|
+
historyIndex: await getGitHistoryIndex(rootDirectoryPath, projectConfig),
|
|
2011
|
+
links: getRepoLinks(rootDirectoryPath),
|
|
2012
|
+
repositoryRootDirectoryPath: getRepositoryRootDirectoryPath(rootDirectoryPath),
|
|
2013
|
+
repositorySourceRootDirectoryPath: getRepositorySourceRootDirectoryPath(rootDirectoryPath),
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
async function readJsonFile<T>(filePath: string): Promise<T | undefined> {
|
|
2018
|
+
try {
|
|
2019
|
+
return JSON.parse(await fs.promises.readFile(filePath, "utf8")) as T;
|
|
2020
|
+
} catch (_error) {
|
|
2021
|
+
return undefined;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
function getOutputRelativeDirectory(projectConfig: any, set?: string) {
|
|
2026
|
+
return projectConfig.sets ? path.join("sets", set || "") : "root";
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
function getDataOutputDirectoryPath(session: CatalogDevSession, projectConfig: any, set?: string) {
|
|
2030
|
+
return path.join(
|
|
2031
|
+
session.outputDirectoryPath,
|
|
2032
|
+
"data",
|
|
2033
|
+
getOutputRelativeDirectory(projectConfig, set),
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function getEntityKeyFromChangedPath(
|
|
2038
|
+
rootDirectoryPath: string,
|
|
2039
|
+
projectConfig: any,
|
|
2040
|
+
changedPath: string,
|
|
2041
|
+
): EntityPathInfo | undefined {
|
|
2042
|
+
const relativePath = path.relative(rootDirectoryPath, changedPath);
|
|
2043
|
+
|
|
2044
|
+
if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
2045
|
+
return undefined;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
return getEntityInfoFromRelativePath(rootDirectoryPath, projectConfig, relativePath);
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
function getChangedPathSummary(rootDirectoryPath: string, changedPaths: string[]) {
|
|
2052
|
+
return changedPaths
|
|
2053
|
+
.slice(0, 3)
|
|
2054
|
+
.map((changedPath) => formatCatalogPath(rootDirectoryPath, changedPath))
|
|
2055
|
+
.join(", ");
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
function classifyCatalogDevChanges(
|
|
2059
|
+
rootDirectoryPath: string,
|
|
2060
|
+
projectConfig: any,
|
|
2061
|
+
changedPaths: string[],
|
|
2062
|
+
options: { withTranslationSearch: boolean; withDuplicates: boolean },
|
|
2063
|
+
): CatalogDevRebuildRequest {
|
|
2064
|
+
const reason = getChangedPathSummary(rootDirectoryPath, changedPaths) || "project changes";
|
|
2065
|
+
const infos = changedPaths.map((changedPath) =>
|
|
2066
|
+
getEntityKeyFromChangedPath(rootDirectoryPath, projectConfig, changedPath),
|
|
2067
|
+
);
|
|
2068
|
+
|
|
2069
|
+
if (infos.length === 0 || infos.some((info) => !info)) {
|
|
2070
|
+
return { kind: "full", reason };
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
const sets = new Set(infos.map((info) => info?.set || ""));
|
|
2074
|
+
const types = new Set(infos.map((info) => info?.type));
|
|
2075
|
+
|
|
2076
|
+
if (sets.size > 1) {
|
|
2077
|
+
return { kind: "full", reason };
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
const set = Array.from(sets)[0] || undefined;
|
|
2081
|
+
|
|
2082
|
+
if (
|
|
2083
|
+
types.size === 1 &&
|
|
2084
|
+
types.has("message") &&
|
|
2085
|
+
!options.withTranslationSearch &&
|
|
2086
|
+
!options.withDuplicates
|
|
2087
|
+
) {
|
|
2088
|
+
return {
|
|
2089
|
+
kind: "message",
|
|
2090
|
+
reason,
|
|
2091
|
+
set,
|
|
2092
|
+
messageKeys: sortStrings(infos.map((info) => info?.key || "").filter(Boolean)),
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
if (
|
|
2097
|
+
projectConfig.sets &&
|
|
2098
|
+
set &&
|
|
2099
|
+
types.size > 0 &&
|
|
2100
|
+
!types.has("test") &&
|
|
2101
|
+
!options.withTranslationSearch &&
|
|
2102
|
+
!options.withDuplicates
|
|
2103
|
+
) {
|
|
2104
|
+
return { kind: "set", reason, set };
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
return { kind: "full", reason };
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
async function writeCatalogManifest(
|
|
2111
|
+
writer: CatalogJsonWriter,
|
|
2112
|
+
rootDirectoryPath: string,
|
|
2113
|
+
projectConfig: any,
|
|
2114
|
+
session: CatalogDevSession,
|
|
2115
|
+
options: {
|
|
2116
|
+
browserRouter: boolean;
|
|
2117
|
+
withTranslationSearch: boolean;
|
|
2118
|
+
withDuplicates: boolean;
|
|
2119
|
+
setIndexes: Record<string, CatalogSetIndex>;
|
|
2120
|
+
executions: Array<{ set: string; projectConfig: any; datasource: any }>;
|
|
2121
|
+
},
|
|
2122
|
+
) {
|
|
2123
|
+
const manifest = {
|
|
2124
|
+
schemaVersion: CATALOG_SCHEMA_VERSION,
|
|
2125
|
+
generatedAt: new Date().toISOString(),
|
|
2126
|
+
router: options.browserRouter === false ? "hash" : "browser",
|
|
2127
|
+
sets: projectConfig.sets,
|
|
2128
|
+
setKeys: projectConfig.sets ? options.executions.map((execution) => execution.set) : [],
|
|
2129
|
+
dev: { editors: session.devEditors },
|
|
2130
|
+
features: {
|
|
2131
|
+
translationSearch: options.withTranslationSearch,
|
|
2132
|
+
duplicates: options.withDuplicates,
|
|
2133
|
+
},
|
|
2134
|
+
links: session.links,
|
|
2135
|
+
paths: {
|
|
2136
|
+
projectHistory: "data/project/history/page-1.json",
|
|
2137
|
+
root: projectConfig.sets ? undefined : "data/root/index.json",
|
|
2138
|
+
sets: projectConfig.sets
|
|
2139
|
+
? Object.fromEntries(
|
|
2140
|
+
options.executions.map((execution) => [
|
|
2141
|
+
execution.set,
|
|
2142
|
+
`data/sets/${encodeURIComponent(execution.set)}/index.json`,
|
|
2143
|
+
]),
|
|
2144
|
+
)
|
|
2145
|
+
: undefined,
|
|
2146
|
+
},
|
|
2147
|
+
counts: Object.fromEntries(
|
|
2148
|
+
Object.keys(options.setIndexes).map((key) => [key, options.setIndexes[key].counts]),
|
|
2149
|
+
),
|
|
2150
|
+
};
|
|
2151
|
+
|
|
2152
|
+
await writer.write(path.join(session.outputDirectoryPath, "data", "manifest.json"), manifest);
|
|
2153
|
+
return manifest;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function getMessageRelationshipFingerprint(message: Message) {
|
|
2157
|
+
const attributes = new Set<string>();
|
|
2158
|
+
const segments = new Set<string>();
|
|
2159
|
+
|
|
2160
|
+
for (const override of message.overrides || []) {
|
|
2161
|
+
collectAttributeKeysFromConditions(override.conditions, attributes);
|
|
2162
|
+
collectSegmentKeys(override.segments, segments);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
return {
|
|
2166
|
+
attributes: sortStrings(Array.from(attributes)),
|
|
2167
|
+
segments: sortStrings(Array.from(segments)),
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
function sameStringList(left: string[] = [], right: string[] = []) {
|
|
2172
|
+
if (left.length !== right.length) {
|
|
2173
|
+
return false;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
return left.every((value, index) => value === right[index]);
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
function summarizeMessage(
|
|
2180
|
+
message: Message,
|
|
2181
|
+
messageKey: string,
|
|
2182
|
+
historyIndex: CatalogHistoryIndex,
|
|
2183
|
+
set: string | undefined,
|
|
2184
|
+
targets: string[],
|
|
2185
|
+
) {
|
|
2186
|
+
const directLocales = Object.keys(message.translations || {});
|
|
2187
|
+
const overrideLocalesSet = new Set<string>();
|
|
2188
|
+
|
|
2189
|
+
for (const override of message.overrides || []) {
|
|
2190
|
+
for (const localeKey of Object.keys(override.translations || {})) {
|
|
2191
|
+
overrideLocalesSet.add(localeKey);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
const overrideLocales = sortStrings(Array.from(overrideLocalesSet));
|
|
2196
|
+
|
|
2197
|
+
return getEntitySummary(message, "message", messageKey, historyIndex, set, {
|
|
2198
|
+
targets,
|
|
2199
|
+
...(directLocales.length > 0 ? { locales: sortStrings(directLocales) } : {}),
|
|
2200
|
+
...(overrideLocales.length > 0 ? { overrideLocales } : {}),
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
async function tryRebuildCatalogMessage(
|
|
2205
|
+
runtime: CatalogRuntime,
|
|
2206
|
+
rootDirectoryPath: string,
|
|
2207
|
+
rootProjectConfig: any,
|
|
2208
|
+
projectConfig: any,
|
|
2209
|
+
datasource: any,
|
|
2210
|
+
session: CatalogDevSession,
|
|
2211
|
+
request: CatalogDevRebuildRequest,
|
|
2212
|
+
) {
|
|
2213
|
+
if (request.kind !== "message" || !request.messageKeys || request.messageKeys.length === 0) {
|
|
2214
|
+
return false;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
const dataDirectoryPath = getDataOutputDirectoryPath(session, rootProjectConfig, request.set);
|
|
2218
|
+
const indexPath = path.join(dataDirectoryPath, "index.json");
|
|
2219
|
+
const index = await readJsonFile<CatalogSetIndex>(indexPath);
|
|
2220
|
+
|
|
2221
|
+
if (!index) {
|
|
2222
|
+
return false;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
const [localeKeys, messageKeys, targetKeys] = (await Promise.all([
|
|
2226
|
+
datasource.listLocales(),
|
|
2227
|
+
datasource.listMessages(),
|
|
2228
|
+
datasource.listTargets(),
|
|
2229
|
+
])) as [string[], string[], string[]];
|
|
2230
|
+
const messageKeySet = new Set(messageKeys);
|
|
2231
|
+
|
|
2232
|
+
if (request.messageKeys.some((messageKey) => !messageKeySet.has(messageKey))) {
|
|
2233
|
+
return false;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
const [locales, targets] = await Promise.all([
|
|
2237
|
+
readAll<Locale>(localeKeys, (key) => datasource.readLocale(key)),
|
|
2238
|
+
readAll<Target>(targetKeys, (key) => datasource.readTarget(key)),
|
|
2239
|
+
]);
|
|
2240
|
+
const localeDirections = getLocaleDirections(locales);
|
|
2241
|
+
const targetMessages = Object.fromEntries(
|
|
2242
|
+
targetKeys.map((targetKey) => [
|
|
2243
|
+
targetKey,
|
|
2244
|
+
getTargetMessageKeys(targets[targetKey], messageKeys),
|
|
2245
|
+
]),
|
|
2246
|
+
) as Record<string, string[]>;
|
|
2247
|
+
const writer = new CatalogJsonWriter();
|
|
2248
|
+
|
|
2249
|
+
for (const messageKey of request.messageKeys) {
|
|
2250
|
+
const oldDetailPath = path.join(
|
|
2251
|
+
dataDirectoryPath,
|
|
2252
|
+
"entities",
|
|
2253
|
+
"message",
|
|
2254
|
+
`${encodeKey(messageKey)}.json`,
|
|
2255
|
+
);
|
|
2256
|
+
const oldDetail = await readJsonFile<any>(oldDetailPath);
|
|
2257
|
+
|
|
2258
|
+
if (!oldDetail) {
|
|
2259
|
+
return false;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
const message = await datasource.readMessage(messageKey);
|
|
2263
|
+
const messageTargets = sortStrings(
|
|
2264
|
+
targetKeys.filter((targetKey) => targetMessages[targetKey].includes(messageKey)),
|
|
2265
|
+
);
|
|
2266
|
+
|
|
2267
|
+
if (!sameStringList(sortStrings(oldDetail.targets || []), messageTargets)) {
|
|
2268
|
+
return false;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
const oldRelationshipFingerprint = getMessageRelationshipFingerprint(oldDetail.entity || {});
|
|
2272
|
+
const nextRelationshipFingerprint = getMessageRelationshipFingerprint(message);
|
|
2273
|
+
|
|
2274
|
+
if (
|
|
2275
|
+
!sameStringList(
|
|
2276
|
+
oldRelationshipFingerprint.attributes,
|
|
2277
|
+
nextRelationshipFingerprint.attributes,
|
|
2278
|
+
) ||
|
|
2279
|
+
!sameStringList(oldRelationshipFingerprint.segments, nextRelationshipFingerprint.segments)
|
|
2280
|
+
) {
|
|
2281
|
+
return false;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
const examples = await runtime.resolveExamples(projectConfig, datasource, {
|
|
2285
|
+
set: request.set,
|
|
2286
|
+
message: messageKey,
|
|
2287
|
+
onlyMessages: true,
|
|
2288
|
+
});
|
|
2289
|
+
const overrides = (message.overrides || []).map((override: Override) => {
|
|
2290
|
+
const attributes = new Set<string>();
|
|
2291
|
+
const overrideSegments = new Set<string>();
|
|
2292
|
+
collectAttributeKeysFromConditions(override.conditions, attributes);
|
|
2293
|
+
collectSegmentKeys(override.segments, overrideSegments);
|
|
2294
|
+
|
|
2295
|
+
return {
|
|
2296
|
+
...override,
|
|
2297
|
+
usedAttributes: sortStrings(Array.from(attributes)),
|
|
2298
|
+
usedSegments: sortStrings(Array.from(overrideSegments)),
|
|
2299
|
+
};
|
|
2300
|
+
});
|
|
2301
|
+
const sourceFileInfo = getSourceFileInfo(
|
|
2302
|
+
session.repositorySourceRootDirectoryPath,
|
|
2303
|
+
rootDirectoryPath,
|
|
2304
|
+
projectConfig,
|
|
2305
|
+
"message",
|
|
2306
|
+
messageKey,
|
|
2307
|
+
{ resolveAbsolutePath: session.devEditors.length > 0 },
|
|
2308
|
+
);
|
|
2309
|
+
const detail = {
|
|
2310
|
+
type: "message",
|
|
2311
|
+
key: messageKey,
|
|
2312
|
+
entity: { ...message, overrides },
|
|
2313
|
+
sourcePath: sourceFileInfo.sourcePath,
|
|
2314
|
+
editLinks: getEditorLinks(session.devEditors, sourceFileInfo),
|
|
2315
|
+
targets: messageTargets,
|
|
2316
|
+
localeKeys,
|
|
2317
|
+
localeDirections,
|
|
2318
|
+
translations: localeKeys.map((localeKey) =>
|
|
2319
|
+
resolveTranslationRow(message.translations, localeKey, locales),
|
|
2320
|
+
),
|
|
2321
|
+
evaluatedExamples: examples.messages,
|
|
2322
|
+
overrideTranslations: overrides.map((override) => ({
|
|
2323
|
+
key: override.key,
|
|
2324
|
+
rows: localeKeys.map((localeKey) =>
|
|
2325
|
+
resolveTranslationRow(override.translations, localeKey, locales),
|
|
2326
|
+
),
|
|
2327
|
+
})),
|
|
2328
|
+
lastModified: getLastModified(session.historyIndex, "message", messageKey, request.set),
|
|
2329
|
+
};
|
|
2330
|
+
|
|
2331
|
+
await writer.write(oldDetailPath, detail);
|
|
2332
|
+
|
|
2333
|
+
await writeHistoryPages(
|
|
2334
|
+
writer,
|
|
2335
|
+
path.join(dataDirectoryPath, "history", "message", encodeKey(messageKey)),
|
|
2336
|
+
getHistoryForEntity(session.historyIndex, "message", messageKey, request.set),
|
|
2337
|
+
{ skipEmpty: true },
|
|
2338
|
+
);
|
|
2339
|
+
|
|
2340
|
+
const nextSummary = summarizeMessage(
|
|
2341
|
+
message,
|
|
2342
|
+
messageKey,
|
|
2343
|
+
session.historyIndex,
|
|
2344
|
+
request.set,
|
|
2345
|
+
messageTargets,
|
|
2346
|
+
);
|
|
2347
|
+
const existingSummaryIndex = index.entities.message.findIndex(
|
|
2348
|
+
(entry) => entry.key === messageKey,
|
|
2349
|
+
);
|
|
2350
|
+
|
|
2351
|
+
if (existingSummaryIndex === -1) {
|
|
2352
|
+
index.entities.message.push(nextSummary);
|
|
2353
|
+
} else {
|
|
2354
|
+
index.entities.message[existingSummaryIndex] = nextSummary;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
index.entities.message.sort((left, right) => left.key.localeCompare(right.key));
|
|
2359
|
+
index.counts.message = messageKeys.length;
|
|
2360
|
+
await writer.write(indexPath, index);
|
|
2361
|
+
|
|
2362
|
+
return true;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
async function rebuildCatalogSetForDev(
|
|
2366
|
+
runtime: CatalogRuntime,
|
|
2367
|
+
rootDirectoryPath: string,
|
|
2368
|
+
projectConfig: any,
|
|
2369
|
+
datasource: any,
|
|
2370
|
+
session: CatalogDevSession,
|
|
2371
|
+
options: {
|
|
2372
|
+
set?: string;
|
|
2373
|
+
browserRouter: boolean;
|
|
2374
|
+
withTranslationSearch: boolean;
|
|
2375
|
+
withDuplicates: boolean;
|
|
2376
|
+
},
|
|
2377
|
+
) {
|
|
2378
|
+
const writer = new CatalogJsonWriter();
|
|
2379
|
+
const progress = new CatalogProgressReporter(rootDirectoryPath, session.outputDirectoryPath);
|
|
2380
|
+
const executions = await runtime.getProjectSetExecutions(projectConfig, datasource);
|
|
2381
|
+
const setIndexes: Record<string, CatalogSetIndex> = {};
|
|
2382
|
+
const existingIndexes = await Promise.all(
|
|
2383
|
+
executions.map(async (execution) => {
|
|
2384
|
+
const indexPath = path.join(
|
|
2385
|
+
session.outputDirectoryPath,
|
|
2386
|
+
"data",
|
|
2387
|
+
getOutputRelativeDirectory(projectConfig, execution.set),
|
|
2388
|
+
"index.json",
|
|
2389
|
+
);
|
|
2390
|
+
return [execution.set || "root", await readJsonFile<CatalogSetIndex>(indexPath)] as const;
|
|
2391
|
+
}),
|
|
2392
|
+
);
|
|
2393
|
+
|
|
2394
|
+
for (const [key, index] of existingIndexes) {
|
|
2395
|
+
if (index) {
|
|
2396
|
+
setIndexes[key] = index;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
const execution = executions.find((item) => (item.set || undefined) === options.set);
|
|
2401
|
+
|
|
2402
|
+
if (!execution) {
|
|
2403
|
+
return false;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
const outputRelativeDirectory = getOutputRelativeDirectory(projectConfig, execution.set);
|
|
2407
|
+
await fs.promises.rm(path.join(session.outputDirectoryPath, "data", outputRelativeDirectory), {
|
|
2408
|
+
recursive: true,
|
|
2409
|
+
force: true,
|
|
2410
|
+
});
|
|
2411
|
+
|
|
2412
|
+
const context: CatalogBuildContext = {
|
|
2413
|
+
rootDirectoryPath,
|
|
2414
|
+
repositoryRootDirectoryPath: session.repositoryRootDirectoryPath,
|
|
2415
|
+
repositorySourceRootDirectoryPath: session.repositorySourceRootDirectoryPath,
|
|
2416
|
+
outputDirectoryPath: session.outputDirectoryPath,
|
|
2417
|
+
dataDirectoryPath: path.join(session.outputDirectoryPath, "data"),
|
|
2418
|
+
historyIndex: session.historyIndex,
|
|
2419
|
+
runtime,
|
|
2420
|
+
devEditors: session.devEditors,
|
|
2421
|
+
duplicateResultsBySet: {},
|
|
2422
|
+
withTranslationSearch: options.withTranslationSearch,
|
|
2423
|
+
withDuplicates: options.withDuplicates,
|
|
2424
|
+
progress,
|
|
2425
|
+
writer,
|
|
2426
|
+
};
|
|
2427
|
+
|
|
2428
|
+
setIndexes[execution.set || "root"] = await buildSetCatalog(
|
|
2429
|
+
context,
|
|
2430
|
+
execution.set,
|
|
2431
|
+
execution.projectConfig,
|
|
2432
|
+
execution.datasource,
|
|
2433
|
+
outputRelativeDirectory,
|
|
2434
|
+
);
|
|
2435
|
+
|
|
2436
|
+
await writeCatalogManifest(writer, rootDirectoryPath, projectConfig, session, {
|
|
2437
|
+
browserRouter: options.browserRouter,
|
|
2438
|
+
withTranslationSearch: options.withTranslationSearch,
|
|
2439
|
+
withDuplicates: options.withDuplicates,
|
|
2440
|
+
setIndexes,
|
|
2441
|
+
executions,
|
|
2442
|
+
});
|
|
2443
|
+
|
|
2444
|
+
return true;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
1980
2447
|
export async function exportCatalog(
|
|
1981
2448
|
runtime: CatalogRuntime,
|
|
1982
2449
|
rootDirectoryPath: string,
|
|
@@ -2003,7 +2470,11 @@ export async function exportCatalog(
|
|
|
2003
2470
|
});
|
|
2004
2471
|
|
|
2005
2472
|
let stepStartedAt = progress.step("Preparing output directory");
|
|
2006
|
-
|
|
2473
|
+
if (options.preserveAssets) {
|
|
2474
|
+
await fs.promises.rm(dataDirectoryPath, { recursive: true, force: true });
|
|
2475
|
+
} else {
|
|
2476
|
+
await fs.promises.rm(outputDirectoryPath, { recursive: true, force: true });
|
|
2477
|
+
}
|
|
2007
2478
|
await fs.promises.mkdir(dataDirectoryPath, { recursive: true });
|
|
2008
2479
|
progress.done(stepStartedAt);
|
|
2009
2480
|
|
|
@@ -2013,13 +2484,17 @@ export async function exportCatalog(
|
|
|
2013
2484
|
progress.done(stepStartedAt);
|
|
2014
2485
|
}
|
|
2015
2486
|
|
|
2016
|
-
const devEditors = options.dev
|
|
2487
|
+
const devEditors = options.dev
|
|
2488
|
+
? options.devSession?.devEditors || options.devEditors || detectDevEditors()
|
|
2489
|
+
: [];
|
|
2017
2490
|
stepStartedAt = progress.step("Reading Git history");
|
|
2018
|
-
const historyIndex =
|
|
2491
|
+
const historyIndex =
|
|
2492
|
+
options.devSession?.historyIndex ||
|
|
2493
|
+
(await getGitHistoryIndex(rootDirectoryPath, projectConfig));
|
|
2019
2494
|
progress.done(stepStartedAt, `(${pluralize(historyIndex.entries.length, "commit")})`);
|
|
2020
2495
|
|
|
2021
2496
|
stepStartedAt = progress.step("Resolving repository links");
|
|
2022
|
-
const links = getRepoLinks(rootDirectoryPath);
|
|
2497
|
+
const links = options.devSession?.links || getRepoLinks(rootDirectoryPath);
|
|
2023
2498
|
progress.done(stepStartedAt);
|
|
2024
2499
|
|
|
2025
2500
|
let duplicateResultsBySet: Record<string, CatalogDuplicateTranslationsSetResult> = {};
|
|
@@ -2050,8 +2525,12 @@ export async function exportCatalog(
|
|
|
2050
2525
|
|
|
2051
2526
|
const context: CatalogBuildContext = {
|
|
2052
2527
|
rootDirectoryPath,
|
|
2053
|
-
repositoryRootDirectoryPath:
|
|
2054
|
-
|
|
2528
|
+
repositoryRootDirectoryPath:
|
|
2529
|
+
options.devSession?.repositoryRootDirectoryPath ||
|
|
2530
|
+
getRepositoryRootDirectoryPath(rootDirectoryPath),
|
|
2531
|
+
repositorySourceRootDirectoryPath:
|
|
2532
|
+
options.devSession?.repositorySourceRootDirectoryPath ||
|
|
2533
|
+
getRepositorySourceRootDirectoryPath(rootDirectoryPath),
|
|
2055
2534
|
outputDirectoryPath,
|
|
2056
2535
|
dataDirectoryPath,
|
|
2057
2536
|
historyIndex,
|
|
@@ -2177,11 +2656,34 @@ function injectCatalogLiveReloadClient(html: string) {
|
|
|
2177
2656
|
return `${html}${script}`;
|
|
2178
2657
|
}
|
|
2179
2658
|
|
|
2180
|
-
function
|
|
2659
|
+
function getCatalogInputWatchPaths(rootDirectoryPath: string, projectConfig: any) {
|
|
2660
|
+
const paths = [path.join(rootDirectoryPath, "messagevisor.config.js")];
|
|
2661
|
+
|
|
2662
|
+
if (projectConfig.sets) {
|
|
2663
|
+
paths.push(projectConfig.setsDirectoryPath);
|
|
2664
|
+
return paths;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
paths.push(
|
|
2668
|
+
projectConfig.localesDirectoryPath,
|
|
2669
|
+
projectConfig.messagesDirectoryPath,
|
|
2670
|
+
projectConfig.attributesDirectoryPath,
|
|
2671
|
+
projectConfig.segmentsDirectoryPath,
|
|
2672
|
+
projectConfig.targetsDirectoryPath,
|
|
2673
|
+
projectConfig.testsDirectoryPath,
|
|
2674
|
+
);
|
|
2675
|
+
|
|
2676
|
+
return paths.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
function createCatalogInputWatcher(
|
|
2181
2680
|
rootDirectoryPath: string,
|
|
2681
|
+
projectConfig: any,
|
|
2182
2682
|
ignoredDirectoryPaths: string[],
|
|
2183
|
-
onChange: (
|
|
2683
|
+
onChange: (changedPaths: string[]) => void,
|
|
2184
2684
|
) {
|
|
2685
|
+
const watchPaths = getCatalogInputWatchPaths(rootDirectoryPath, projectConfig);
|
|
2686
|
+
|
|
2185
2687
|
function shouldIgnore(targetPath: string) {
|
|
2186
2688
|
const resolvedTargetPath = path.resolve(targetPath);
|
|
2187
2689
|
|
|
@@ -2195,7 +2697,24 @@ function createProjectWatcher(
|
|
|
2195
2697
|
});
|
|
2196
2698
|
}
|
|
2197
2699
|
|
|
2198
|
-
function
|
|
2700
|
+
function shouldWatch(targetPath: string) {
|
|
2701
|
+
const resolvedTargetPath = path.resolve(targetPath);
|
|
2702
|
+
|
|
2703
|
+
if (shouldIgnore(resolvedTargetPath)) {
|
|
2704
|
+
return false;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
return watchPaths.some((watchPath) => {
|
|
2708
|
+
const resolvedWatchPath = path.resolve(watchPath);
|
|
2709
|
+
|
|
2710
|
+
return (
|
|
2711
|
+
resolvedTargetPath === resolvedWatchPath ||
|
|
2712
|
+
resolvedTargetPath.startsWith(`${resolvedWatchPath}${path.sep}`)
|
|
2713
|
+
);
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
function collectSnapshotEntries(directoryPath: string, snapshotEntries: Map<string, string>) {
|
|
2199
2718
|
if (shouldIgnore(directoryPath)) {
|
|
2200
2719
|
return;
|
|
2201
2720
|
}
|
|
@@ -2226,8 +2745,7 @@ function createProjectWatcher(
|
|
|
2226
2745
|
|
|
2227
2746
|
try {
|
|
2228
2747
|
const stat = fs.statSync(entryPath);
|
|
2229
|
-
|
|
2230
|
-
snapshotEntries.push(`${relativePath}:${stat.size}:${stat.mtimeMs}`);
|
|
2748
|
+
snapshotEntries.set(entryPath, `${stat.size}:${stat.mtimeMs}`);
|
|
2231
2749
|
} catch {
|
|
2232
2750
|
// Ignore transient editor save races.
|
|
2233
2751
|
}
|
|
@@ -2235,26 +2753,108 @@ function createProjectWatcher(
|
|
|
2235
2753
|
}
|
|
2236
2754
|
|
|
2237
2755
|
function createSnapshot() {
|
|
2238
|
-
const snapshotEntries
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2756
|
+
const snapshotEntries = new Map<string, string>();
|
|
2757
|
+
|
|
2758
|
+
for (const watchPath of watchPaths) {
|
|
2759
|
+
if (!fs.existsSync(watchPath)) {
|
|
2760
|
+
continue;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
const stat = fs.statSync(watchPath);
|
|
2764
|
+
|
|
2765
|
+
if (stat.isFile()) {
|
|
2766
|
+
snapshotEntries.set(watchPath, `${stat.size}:${stat.mtimeMs}`);
|
|
2767
|
+
continue;
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
collectSnapshotEntries(watchPath, snapshotEntries);
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
return snapshotEntries;
|
|
2242
2774
|
}
|
|
2243
2775
|
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
const nextSnapshot = createSnapshot();
|
|
2776
|
+
function getSnapshotChanges(previous: Map<string, string>, next: Map<string, string>) {
|
|
2777
|
+
const changedPaths = new Set<string>();
|
|
2247
2778
|
|
|
2248
|
-
|
|
2249
|
-
|
|
2779
|
+
for (const [filePath, signature] of Array.from(next.entries())) {
|
|
2780
|
+
if (previous.get(filePath) !== signature) {
|
|
2781
|
+
changedPaths.add(filePath);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
for (const filePath of Array.from(previous.keys())) {
|
|
2786
|
+
if (!next.has(filePath)) {
|
|
2787
|
+
changedPaths.add(filePath);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
return Array.from(changedPaths);
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
function createPollingWatcher() {
|
|
2795
|
+
let previousSnapshot = createSnapshot();
|
|
2796
|
+
const interval = setInterval(() => {
|
|
2797
|
+
const nextSnapshot = createSnapshot();
|
|
2798
|
+
const changedPaths = getSnapshotChanges(previousSnapshot, nextSnapshot).filter(shouldWatch);
|
|
2799
|
+
|
|
2800
|
+
previousSnapshot = nextSnapshot;
|
|
2801
|
+
|
|
2802
|
+
if (changedPaths.length === 0) {
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
onChange(changedPaths);
|
|
2807
|
+
}, 1000);
|
|
2808
|
+
|
|
2809
|
+
return () => {
|
|
2810
|
+
clearInterval(interval);
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
const watchers: fs.FSWatcher[] = [];
|
|
2815
|
+
let nativeWatcherFailed = false;
|
|
2816
|
+
|
|
2817
|
+
for (const watchPath of watchPaths) {
|
|
2818
|
+
if (!fs.existsSync(watchPath)) {
|
|
2819
|
+
continue;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
try {
|
|
2823
|
+
const stat = fs.statSync(watchPath);
|
|
2824
|
+
const directoryPath = stat.isDirectory() ? watchPath : path.dirname(watchPath);
|
|
2825
|
+
const watcher = fs.watch(
|
|
2826
|
+
directoryPath,
|
|
2827
|
+
{ recursive: stat.isDirectory() },
|
|
2828
|
+
(_eventType, filename) => {
|
|
2829
|
+
const changedPath = filename
|
|
2830
|
+
? path.resolve(directoryPath, filename.toString())
|
|
2831
|
+
: directoryPath;
|
|
2832
|
+
|
|
2833
|
+
if (shouldWatch(changedPath)) {
|
|
2834
|
+
onChange([changedPath]);
|
|
2835
|
+
}
|
|
2836
|
+
},
|
|
2837
|
+
);
|
|
2838
|
+
|
|
2839
|
+
watchers.push(watcher);
|
|
2840
|
+
} catch (_error) {
|
|
2841
|
+
nativeWatcherFailed = true;
|
|
2842
|
+
break;
|
|
2250
2843
|
}
|
|
2844
|
+
}
|
|
2251
2845
|
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2846
|
+
if (nativeWatcherFailed || watchers.length === 0) {
|
|
2847
|
+
for (const watcher of watchers) {
|
|
2848
|
+
watcher.close();
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
return createPollingWatcher();
|
|
2852
|
+
}
|
|
2255
2853
|
|
|
2256
2854
|
return () => {
|
|
2257
|
-
|
|
2855
|
+
for (const watcher of watchers) {
|
|
2856
|
+
watcher.close();
|
|
2857
|
+
}
|
|
2258
2858
|
};
|
|
2259
2859
|
}
|
|
2260
2860
|
|
|
@@ -2405,6 +3005,11 @@ function isWithDuplicatesEnabled(parsed: CatalogPluginParsedOptions) {
|
|
|
2405
3005
|
return parsed.withDuplicates === true || parsed["with-duplicates"] === true;
|
|
2406
3006
|
}
|
|
2407
3007
|
|
|
3008
|
+
export const __catalogDevInternals = {
|
|
3009
|
+
classifyCatalogDevChanges,
|
|
3010
|
+
getCatalogInputWatchPaths,
|
|
3011
|
+
};
|
|
3012
|
+
|
|
2408
3013
|
export function createCatalogPlugin(
|
|
2409
3014
|
runtime: CatalogRuntime,
|
|
2410
3015
|
api: ReturnType<typeof createCatalogApi> = createCatalogApi(runtime),
|
|
@@ -2418,11 +3023,18 @@ export function createCatalogPlugin(
|
|
|
2418
3023
|
const withDuplicates = isWithDuplicatesEnabled(parsed);
|
|
2419
3024
|
|
|
2420
3025
|
if (!parsed.subcommand) {
|
|
3026
|
+
const outputDirectoryPath = parsed.outDir
|
|
3027
|
+
? path.resolve(rootDirectoryPath, parsed.outDir)
|
|
3028
|
+
: projectConfig.catalogDirectoryPath;
|
|
3029
|
+
const devSession = await createCatalogDevSession(rootDirectoryPath, projectConfig, {
|
|
3030
|
+
outDir: parsed.outDir,
|
|
3031
|
+
});
|
|
2421
3032
|
await api.exportCatalog(rootDirectoryPath, projectConfig, datasource, {
|
|
2422
3033
|
outDir: parsed.outDir,
|
|
2423
3034
|
copyAssets: !parsed.noAssets,
|
|
2424
3035
|
browserRouter,
|
|
2425
3036
|
dev: true,
|
|
3037
|
+
devSession,
|
|
2426
3038
|
withTranslationSearch,
|
|
2427
3039
|
withDuplicates,
|
|
2428
3040
|
});
|
|
@@ -2433,9 +3045,6 @@ export function createCatalogPlugin(
|
|
|
2433
3045
|
liveReload: true,
|
|
2434
3046
|
});
|
|
2435
3047
|
|
|
2436
|
-
const outputDirectoryPath = parsed.outDir
|
|
2437
|
-
? path.resolve(rootDirectoryPath, parsed.outDir)
|
|
2438
|
-
: projectConfig.catalogDirectoryPath;
|
|
2439
3048
|
const ignoredDirectoryPaths = [
|
|
2440
3049
|
path.join(rootDirectoryPath, ".git"),
|
|
2441
3050
|
path.join(rootDirectoryPath, "node_modules"),
|
|
@@ -2447,29 +3056,77 @@ export function createCatalogPlugin(
|
|
|
2447
3056
|
outputDirectoryPath,
|
|
2448
3057
|
];
|
|
2449
3058
|
let exportInFlight = false;
|
|
2450
|
-
let
|
|
2451
|
-
let
|
|
3059
|
+
let queuedChanges: string[] = [];
|
|
3060
|
+
let pendingChanges: string[] = [];
|
|
2452
3061
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
2453
3062
|
|
|
2454
|
-
const
|
|
3063
|
+
const runRebuildAndReload = async (changedPaths: string[]) => {
|
|
2455
3064
|
if (exportInFlight) {
|
|
2456
|
-
|
|
2457
|
-
queuedReason = queuedReason || reason;
|
|
3065
|
+
queuedChanges.push(...changedPaths);
|
|
2458
3066
|
return;
|
|
2459
3067
|
}
|
|
2460
3068
|
|
|
2461
3069
|
exportInFlight = true;
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
copyAssets: !parsed.noAssets,
|
|
2468
|
-
browserRouter,
|
|
2469
|
-
dev: true,
|
|
3070
|
+
const request = classifyCatalogDevChanges(
|
|
3071
|
+
rootDirectoryPath,
|
|
3072
|
+
projectConfig,
|
|
3073
|
+
changedPaths,
|
|
3074
|
+
{
|
|
2470
3075
|
withTranslationSearch,
|
|
2471
3076
|
withDuplicates,
|
|
2472
|
-
}
|
|
3077
|
+
},
|
|
3078
|
+
);
|
|
3079
|
+
console.log(`\n[catalog] Rebuilding (${request.kind}) because ${request.reason}`);
|
|
3080
|
+
|
|
3081
|
+
try {
|
|
3082
|
+
let handled = false;
|
|
3083
|
+
|
|
3084
|
+
if (request.kind === "message") {
|
|
3085
|
+
const [execution] = await runtime.getProjectSetExecutions(
|
|
3086
|
+
projectConfig,
|
|
3087
|
+
datasource,
|
|
3088
|
+
request.set,
|
|
3089
|
+
);
|
|
3090
|
+
handled = await tryRebuildCatalogMessage(
|
|
3091
|
+
runtime,
|
|
3092
|
+
rootDirectoryPath,
|
|
3093
|
+
projectConfig,
|
|
3094
|
+
execution.projectConfig,
|
|
3095
|
+
execution.datasource,
|
|
3096
|
+
devSession,
|
|
3097
|
+
request,
|
|
3098
|
+
);
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
if (!handled && request.kind === "set" && request.set) {
|
|
3102
|
+
handled = await rebuildCatalogSetForDev(
|
|
3103
|
+
runtime,
|
|
3104
|
+
rootDirectoryPath,
|
|
3105
|
+
projectConfig,
|
|
3106
|
+
datasource,
|
|
3107
|
+
devSession,
|
|
3108
|
+
{
|
|
3109
|
+
set: request.set,
|
|
3110
|
+
browserRouter,
|
|
3111
|
+
withTranslationSearch,
|
|
3112
|
+
withDuplicates,
|
|
3113
|
+
},
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
if (!handled) {
|
|
3118
|
+
await api.exportCatalog(rootDirectoryPath, projectConfig, datasource, {
|
|
3119
|
+
outDir: parsed.outDir,
|
|
3120
|
+
copyAssets: false,
|
|
3121
|
+
preserveAssets: true,
|
|
3122
|
+
browserRouter,
|
|
3123
|
+
dev: true,
|
|
3124
|
+
devSession,
|
|
3125
|
+
withTranslationSearch,
|
|
3126
|
+
withDuplicates,
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
|
|
2473
3130
|
server.triggerReload();
|
|
2474
3131
|
} catch (error) {
|
|
2475
3132
|
console.error("[catalog] Export failed during watch mode");
|
|
@@ -2477,28 +3134,30 @@ export function createCatalogPlugin(
|
|
|
2477
3134
|
} finally {
|
|
2478
3135
|
exportInFlight = false;
|
|
2479
3136
|
|
|
2480
|
-
if (
|
|
2481
|
-
const
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
void runExportAndReload(nextReason);
|
|
3137
|
+
if (queuedChanges.length > 0) {
|
|
3138
|
+
const nextChanges = queuedChanges;
|
|
3139
|
+
queuedChanges = [];
|
|
3140
|
+
void runRebuildAndReload(nextChanges);
|
|
2485
3141
|
}
|
|
2486
3142
|
}
|
|
2487
3143
|
};
|
|
2488
3144
|
|
|
2489
|
-
const stopWatchingProject =
|
|
3145
|
+
const stopWatchingProject = createCatalogInputWatcher(
|
|
2490
3146
|
rootDirectoryPath,
|
|
3147
|
+
projectConfig,
|
|
2491
3148
|
ignoredDirectoryPaths,
|
|
2492
|
-
(
|
|
2493
|
-
|
|
3149
|
+
(changedPaths) => {
|
|
3150
|
+
pendingChanges.push(...changedPaths);
|
|
2494
3151
|
|
|
2495
3152
|
if (debounceTimer) {
|
|
2496
3153
|
clearTimeout(debounceTimer);
|
|
2497
3154
|
}
|
|
2498
3155
|
debounceTimer = setTimeout(() => {
|
|
3156
|
+
const nextChanges = Array.from(new Set(pendingChanges));
|
|
3157
|
+
pendingChanges = [];
|
|
2499
3158
|
debounceTimer = null;
|
|
2500
|
-
void
|
|
2501
|
-
},
|
|
3159
|
+
void runRebuildAndReload(nextChanges);
|
|
3160
|
+
}, 250);
|
|
2502
3161
|
},
|
|
2503
3162
|
);
|
|
2504
3163
|
|