Skip to content

Commit 0aebd79

Browse files
committed
Fix support for yaml dictionary files
1 parent a219bee commit 0aebd79

File tree

18 files changed

+107
-62
lines changed

18 files changed

+107
-62
lines changed

examples/starlight/lunaria.config.json

-23
This file was deleted.

examples/starlight/lunaria.config.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ export default defineConfig({
1717
},
1818
type: 'universal',
1919
},
20+
{
21+
include: ['src/content/i18n/en.yml'],
22+
pattern: {
23+
source: 'src/content/i18n/@lang.yml',
24+
locales: 'src/content/i18n/@lang.yml',
25+
},
26+
type: 'dictionary',
27+
},
2028
],
2129
tracking: {
2230
localizableProperty: 'i18nReady',
@@ -29,5 +37,4 @@ export default defineConfig({
2937
'i18nIgnore',
3038
],
3139
},
32-
external: true,
3340
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
one: test
2+
second: test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
one: teste

examples/starlight/test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ const lunaria = await createLunaria({
55
force: true,
66
logLevel: 'debug',
77
});
8-
const status = await lunaria.getFullStatus();
9-
console.info(status);
8+
const status = await lunaria.getFileStatus('src/content/i18n/en.yml');
9+
console.info(status.localizations[0].missingKeys);
1010
console.timeEnd('Lunaria benchmark');

packages/core/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
"types": "./dist/index.d.ts",
2525
"default": "./dist/index.js"
2626
},
27+
"./utils": {
28+
"types": "./dist/utils/index.d.ts",
29+
"default": "./dist/utils/index.js"
30+
},
2731
"./config": {
2832
"types": "./dist/config/index.d.ts",
2933
"default": "./dist/config/index.js"
@@ -43,6 +47,7 @@
4347
"dependencies": {
4448
"consola": "^3.2.3",
4549
"jiti": "2.3.3",
50+
"js-yaml": "^4.1.0",
4651
"neotraverse": "^0.6.18",
4752
"path-to-regexp": "6.3.0",
4853
"picomatch": "^4.0.2",
@@ -52,6 +57,7 @@
5257
"zod": "^3.23.8"
5358
},
5459
"devDependencies": {
60+
"@types/js-yaml": "^4.0.9",
5561
"@types/node": "^22.5.4",
5662
"@types/picomatch": "^3.0.1"
5763
},

packages/core/src/config/config.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { resolve } from 'node:path';
22
import { ConfigNotFound, ConfigValidationError } from '../errors/errors.js';
3-
import { moduleLoader } from '../files/loaders.js';
3+
import { loadModule } from '../files/loaders.js';
44
import { LunariaPreSetupSchema } from '../integrations/schema.js';
55
import type { CompleteLunariaUserConfig } from '../integrations/types.js';
66
import { exists, parseWithFriendlyErrors } from '../utils/utils.js';
@@ -38,7 +38,7 @@ export async function loadConfig() {
3838
throw path;
3939
}
4040

41-
const mod = await moduleLoader(path);
41+
const mod = await loadModule(path);
4242
if (mod instanceof Error) {
4343
throw mod;
4444
}

packages/core/src/config/schema.ts

-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ export const SetupOptionsSchema = z.object({
6161
}
6262
>
6363
>,
64-
fileLoader: z.function(z.tuple([z.string()]), z.any()),
6564
});
6665

6766
const LunariaIntegrationSchema = z.object({

packages/core/src/errors/errors.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ export const FailedToLoadModule = {
2828
export const InvalidFilesPattern = {
2929
name: 'InvalidFilesPattern',
3030
title: 'Invalid `files` pattern was found.',
31-
message: (pattern: string, parameter: string) =>
32-
`The file pattern \`${pattern}\` is missing the \`${parameter}\` parameter. Add it to your pattern string.`,
31+
message: (pattern: string) =>
32+
`The file pattern \`${pattern}\` is missing a valid path parameter. Be sure to add at least one to your pattern string.`,
3333
} satisfies ErrorContext;
3434

3535
export const FileConfigNotFound = {
@@ -46,11 +46,18 @@ export const UncommittedFileFound = {
4646
`The file \`${path}\` is being tracked but no commits have been found. Ensure all tracked files in your working branch are committed before running Lunaria.`,
4747
} satisfies ErrorContext;
4848

49-
export const InvalidDictionaryFormat = {
50-
name: 'InvalidDictionaryFormat',
51-
title: 'A file with an invalid dictionary format was found.',
49+
export const InvalidDictionaryStructure = {
50+
name: 'InvalidDictionaryStructure',
51+
title: 'A file with an invalid dictionary structure was found.',
5252
message: (path: string) =>
53-
`The \`type: "dictionary"\` file \`${path}\` has an invalid format. Dictionaries are expected to be a recursive Record of string keys and values. Alternatively, you can track this file without key completion checking by setting it to \`type: "universal"\` instead.`,
53+
`The \`type: "dictionary"\` file \`${path}\` has an invalid structure. Dictionaries are expected to be a recursive Record of string keys and values. Alternatively, you can track this file without key completion checking by setting it to \`type: "universal"\` instead.`,
54+
} satisfies ErrorContext;
55+
56+
export const UnsupportedDictionaryFileFormat = {
57+
name: 'UnsupportedDictionaryFileFormat',
58+
title: 'An unsupported file format was found.',
59+
message: (file: string) =>
60+
`The file \`${file}\` has an unsupported file format. Dictionaries can be Markdown/MDX/Markdoc, JSON, or JavaScript/TypeScript modules. Use one of these file formats or instead track this file without key completion checking by setting it to \`type: "universal"\` instead.`,
5461
} satisfies ErrorContext;
5562

5663
export const UnsupportedIntegrationSelfUpdate = {

packages/core/src/files/loaders.ts

+19-10
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { readFile } from 'node:fs/promises';
22
import { resolve } from 'node:path';
33
import { createJiti } from 'jiti';
4+
import yaml from 'js-yaml';
45
import { parse } from 'ultramatter';
56
import { FailedToLoadModule } from '../errors/errors.js';
67

8+
// TODO: Consider moving this out of the loaders file and directly into the public utils API.
9+
710
/** Regex to match ESM and CJS JavaScript/TypeScript files. */
811
export const moduleFileRe = /\.(c|m)?(ts|js)$/;
912
/** Loader for JavaScript/TypeScript modules (CJS, ESM). */
10-
export async function moduleLoader(path: string) {
13+
export async function loadModule(path: string) {
1114
const resolvedPath = resolve(path);
1215
const jiti = createJiti(import.meta.url);
1316

@@ -19,9 +22,9 @@ export async function moduleLoader(path: string) {
1922
}
2023

2124
/** Regex to match files that support frontmatter. */
22-
export const fileSupportsFrontmatterRe = /\.(yml|md|markdown|mdx|mdoc)$/;
23-
/** Loader for frontmatter in `.yml`, `.md`, `.markdown`, `.mdx`, `.mdoc`. */
24-
export async function frontmatterLoader(path: string) {
25+
export const frontmatterFileRe = /\.(md|markdown|mdx|mdoc)$/;
26+
/** Loader for frontmatter in `.md`, `.markdown`, `.mdx`, `.mdoc`. */
27+
export async function loadFrontmatter(path: string) {
2528
const resolvedPath = resolve(path);
2629

2730
try {
@@ -37,7 +40,7 @@ export async function frontmatterLoader(path: string) {
3740
/** Regex to match JSON files. */
3841
export const jsonFileRe = /\.json$/;
3942
/** Loader for JSON files. */
40-
export async function jsonLoader(path: string) {
43+
export async function loadJSON(path: string) {
4144
const resolvedPath = resolve(path);
4245

4346
try {
@@ -48,9 +51,15 @@ export async function jsonLoader(path: string) {
4851
}
4952
}
5053

51-
/** Loader for JS/TS modules, JSON, and frontmatter. */
52-
export async function fileLoader(path: string) {
53-
if (moduleFileRe.test(path)) return await moduleLoader(path);
54-
if (fileSupportsFrontmatterRe.test(path)) return await frontmatterLoader(path);
55-
if (jsonFileRe.test(path)) return jsonLoader(path);
54+
export const yamlFileRe = /\.(yml|yaml)$/;
55+
/** Loader for YAML files. */
56+
export async function loadYAML(path: string) {
57+
const resolvedPath = resolve(path);
58+
59+
try {
60+
const file = await readFile(resolvedPath, 'utf-8');
61+
return yaml.load(file);
62+
} catch (e) {
63+
if (e instanceof Error) return e;
64+
}
5665
}

packages/core/src/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,10 @@ class Lunaria {
183183
new Date(latestSourceChanges.latestTrackedChange.date) >
184184
new Date(latestLocaleChanges.latestTrackedChange.date);
185185

186-
const entryTypeData = () => {
186+
const entryTypeData = async () => {
187187
if (fileConfig.type === 'dictionary') {
188188
try {
189-
const missingKeys = getDictionaryCompletion(
189+
const missingKeys = await getDictionaryCompletion(
190190
fileConfig.optionalKeys,
191191
externalSafePath(external, this.#cwd, sourcePath),
192192
externalSafePath(external, this.#cwd, localizedPath),
@@ -210,7 +210,7 @@ class Lunaria {
210210
path: localizedPath,
211211
git: latestLocaleChanges,
212212
status: isOutdated ? 'outdated' : 'up-to-date',
213-
...entryTypeData(),
213+
...(await entryTypeData()),
214214
};
215215
}),
216216
),

packages/core/src/integrations/integrations.ts

-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { ConsolaInstance } from 'consola';
22
import { validateFinalConfig, validateInitialConfig } from '../config/config.js';
33
import type { LunariaUserConfig } from '../config/types.js';
44
import { UnsupportedIntegrationSelfUpdate } from '../errors/errors.js';
5-
import { fileLoader } from '../files/loaders.js';
65
import type { CompleteLunariaUserConfig } from './types.js';
76

87
export async function runSetupHook(config: LunariaUserConfig, logger: ConsolaInstance) {
@@ -37,7 +36,6 @@ export async function runSetupHook(config: LunariaUserConfig, logger: ConsolaIns
3736
});
3837
},
3938
logger,
40-
fileLoader,
4139
});
4240
}
4341

packages/core/src/integrations/types.ts

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export interface LunariaIntegration {
88
config: LunariaUserConfig;
99
updateConfig: (config: Partial<LunariaUserConfig>) => void;
1010
logger: ConsolaInstance;
11-
fileLoader: (path: string) => Promise<unknown>;
1211
}) => void | Promise<void>;
1312
};
1413
}

packages/core/src/status/git.ts

+3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ export class LunariaGitInstance {
6666
};
6767
}
6868

69+
// TODO: Using an external repo seems to introduce some sort of performance gains, this should be tested to ensure
70+
// its not a bug e.g. not being able to read certain files or the git history not being complete and missing commits
71+
// that are necessary for the status to be accurate.
6972
async handleExternalRepository() {
7073
const { cloneDir, repository } = this.#config;
7174
const { name, hosting, rootDir } = repository;

packages/core/src/status/status.ts

+33-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
import { Traverse } from 'neotraverse/modern';
22
import type { OptionalKeys } from '../config/types.js';
3-
import { InvalidDictionaryFormat } from '../errors/errors.js';
4-
import { fileLoader, fileSupportsFrontmatterRe, frontmatterLoader } from '../files/loaders.js';
3+
import { InvalidDictionaryStructure, UnsupportedDictionaryFileFormat } from '../errors/errors.js';
4+
import {
5+
frontmatterFileRe,
6+
jsonFileRe,
7+
loadFrontmatter,
8+
loadJSON,
9+
loadModule,
10+
loadYAML,
11+
moduleFileRe,
12+
yamlFileRe,
13+
} from '../files/loaders.js';
514
import { DictionarySchema } from './schema.js';
615
import type { Dictionary } from './types.js';
716

817
export async function isFileLocalizable(path: string, localizableProperty: string | undefined) {
918
// If no localizableProperty is specified, all files are supposed to be localizable.
1019
if (!localizableProperty) return true;
1120
// If the file doesn't support frontmatter, it's automatically supposed to be localizable.
12-
if (!fileSupportsFrontmatterRe.test(path)) return true;
21+
if (!frontmatterFileRe.test(path)) return true;
1322

14-
const frontmatter = await frontmatterLoader(path);
23+
const frontmatter = await loadFrontmatter(path);
1524

1625
if (frontmatter instanceof Error) return frontmatter;
1726

@@ -31,27 +40,25 @@ export async function getDictionaryCompletion(
3140
sourceDictPath: string,
3241
localeDictPath: string,
3342
) {
34-
const sourceDict = await fileLoader(sourceDictPath);
35-
const localeDict = await fileLoader(localeDictPath);
43+
const [sourceDict, localeDict] = await loadDictionaries(sourceDictPath, localeDictPath);
3644

3745
if (sourceDict instanceof Error || localeDict instanceof Error) {
3846
throw sourceDict instanceof Error ? sourceDict : localeDict;
3947
}
4048

4149
const parsedSourceDict = DictionarySchema.safeParse(sourceDict);
4250
if (parsedSourceDict.error) {
43-
throw new Error(InvalidDictionaryFormat.message(sourceDictPath));
51+
throw new Error(InvalidDictionaryStructure.message(sourceDictPath));
4452
}
4553

4654
const parsedLocaleDict = DictionarySchema.safeParse(localeDict);
4755
if (parsedLocaleDict.error) {
48-
throw new Error(InvalidDictionaryFormat.message(localeDictPath));
56+
throw new Error(InvalidDictionaryStructure.message(localeDictPath));
4957
}
5058

5159
return findMissingKeys(optionalKeys, parsedSourceDict.data, parsedLocaleDict.data);
5260
}
5361

54-
// TODO: Test this function.
5562
export function findMissingKeys(
5663
optionalKeys: OptionalKeys | undefined,
5764
sourceDict: Dictionary,
@@ -94,3 +101,20 @@ export function findMissingKeys(
94101

95102
return missingKeys;
96103
}
104+
105+
// TODO: Add integration tests for this function
106+
function loadDictionaries(sourcePath: string, localePath: string) {
107+
if (moduleFileRe.test(sourcePath)) {
108+
return Promise.all([loadModule(sourcePath), loadModule(localePath)]);
109+
}
110+
111+
if (yamlFileRe.test(sourcePath)) {
112+
return Promise.all([loadYAML(sourcePath), loadYAML(localePath)]);
113+
}
114+
115+
if (jsonFileRe.test(sourcePath)) {
116+
return Promise.all([loadJSON(sourcePath), loadJSON(localePath)]);
117+
}
118+
119+
throw new Error(UnsupportedDictionaryFileFormat.message(sourcePath));
120+
}

packages/core/src/utils/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/** Public facing utils */
2+
export { loadFrontmatter, loadJSON, loadModule, loadYAML } from '../files/loaders.js';

packages/core/src/utils/utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { mkdir, stat, writeFile } from 'node:fs/promises';
33
import { join, resolve } from 'node:path';
44
import type { z } from 'zod';
55
import { errorMap } from '../errors/zod-map.js';
6-
import { jsonLoader } from '../files/loaders.js';
6+
import { loadJSON } from '../files/loaders.js';
77

88
export function isRelative(path: string) {
99
return path.startsWith('./') || path.startsWith('../');
@@ -75,7 +75,7 @@ export async function createCache(dir: string, entry: string, hash: string) {
7575
);
7676
};
7777

78-
const contents = async () => await jsonLoader(path);
78+
const contents = async () => await loadJSON(path);
7979

8080
const revalidate = async (hash: string) => {
8181
if ((await contents())?.__validation !== hash) {

0 commit comments

Comments
 (0)