Skip to content

Commit

Permalink
Standardize File Handling
Browse files Browse the repository at this point in the history
  • Loading branch information
LorisSigrist committed Oct 3, 2023
1 parent 629afce commit a2f6646
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 92 deletions.
6 changes: 3 additions & 3 deletions src/adapter/svelte/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ locale.subscribe((value) => { t.set(getMessage) });
if(import.meta.hot) {
import.meta.hot.on("t18s:createLocale", async (data)=>{
import.meta.hot.on("t18s:createLocale", async (data) => {
locales.update((locales) => [...locales, data.locale]);
//Force-reload the module - Add a random query parameter to bust the cache
Expand All @@ -227,7 +227,7 @@ if(import.meta.hot) {
t.set(getMessage); //update the store
});
import.meta.hot.on("t18s:invalidateLocale", async (data)=>{
import.meta.hot.on("t18s:invalidateLocale", async (data) => {
//Force-reload the module - Add a random query parameter to bust the cache
const newMessages = (await import(/* @vite-ignore */ "/@id/__x00__@t18s/messages/" + data.locale + "?" + Math.random())).default;
console.info("[t18s] Reloading locale " + data.locale);
Expand All @@ -236,7 +236,7 @@ if(import.meta.hot) {
t.set(getMessage); //update the store
});
import.meta.hot.on("t18s:removeLocale", async (data)=>{
import.meta.hot.on("t18s:removeLocale", async (data) => {
console.info("[t18s] Removing locale " + data.locale);
delete messages[data.locale];
Expand Down
76 changes: 76 additions & 0 deletions src/formatHandlers/fileHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { readFile } from "fs/promises";
import { basename } from "path";
import { LoadingException } from "./exception.js";

export class FileHandler {
/** @type {import("./types.js").FormatHandler[]} handlers */
#handlers;

/** @param {import("./types.js").FormatHandler[]} handlers */
constructor(handlers) {
this.#handlers = handlers;
}

/**
* @param {string} filePath Absolute path to the file that needs to be handled
* @param {string} locale The locale for which the file should be handled
* @returns {Promise<import("../types.js").Dictionary>} A dictionary
*
* @throws {LoadingException} If the file could not be handled
*/
async handle(filePath, locale) {
const handler = this.#getHandler(filePath);
if (!handler) throw new Error(`Could not find handler for ${filePath}`);
const textContent = await this.#readFileContent(filePath);
const dictionary = await handler.load(filePath, textContent, locale);

return dictionary;
}

/**
* Resolved which handler should be used for the given file
* @param {string} filePath
* @returns {import("./types.js").FormatHandler | null}
*/
#getHandler(filePath) {
const filename = basename(filePath);
const fileExtension = filename.split(".").at(-1);
if (typeof fileExtension !== "string")
throw new LoadingException(
"Could not determine file extension for ${filePath}",
);

const handler = this.#handlers.find((l) =>
l.fileExtensions.includes(fileExtension),
);

return handler ?? null;
}

/**
* Reads the raw text content of the given file
* @param {string} filePath
* @returns {Promise<string>}
*
* @throws {LoadingException} If the file could not be read
*/
async #readFileContent(filePath) {
try {
const textContent = await readFile(filePath, "utf-8");
return textContent;
} catch (e) {
if (!(e instanceof Error)) throw e;
throw new LoadingException(`Failed to read file ${filePath}`, {
cause: e,
});
}
}

/**
* Lists the file extensions for which a handler is available
* @returns {string[]}
*/
getSupportedFileExtensions() {
return this.#handlers.flatMap((h) => h.fileExtensions);
}
}
1 change: 1 addition & 0 deletions src/formatHandlers/json/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const JsonHandler = {
const parsed = JSON.parse(content);
return generateDictionaryFromTree(parsed, locale);
} catch (e) {
if (!(e instanceof Error)) throw e;
throw new LoadingException(
`Could not parse JSON file ${filePath}: ${e.message}`,
{ cause: e },
Expand Down
3 changes: 2 additions & 1 deletion src/formatHandlers/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Dictionary } from "../types.js";
import { LoadingException } from "./exception.js";

export type FormatHandler = {
fileExtensions: string[];
Expand All @@ -8,7 +9,7 @@ export type FormatHandler = {
* @param filePath - The path of this file - Only used in Error messages.
* @param fileContent - The text content of this file.
* @param locale - The locale this file is for.
* @throws {Error} - If the file content is invalid.
* @throws {LoadingException} - If the file content is invalid.
* @returns A dictionary.
*/
load: (
Expand Down
1 change: 1 addition & 0 deletions src/formatHandlers/yaml/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const YamlHandler = {
});
return generateDictionaryFromTree(parsed, locale);
} catch (e) {
if (!(e instanceof Error)) throw e;
throw new LoadingException(
`Could not parse YAML file ${filePath}: ${e.message}`,
{ cause: e },
Expand Down
124 changes: 37 additions & 87 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { basename, resolve } from "path";
import { readFile, readdir, writeFile } from "fs/promises";
import { readdir, writeFile } from "fs/promises";
import { YamlHandler } from "./formatHandlers/yaml/index.js";
import { JsonHandler } from "./formatHandlers/json/index.js";
import { Logger } from "./utils/logger.js";
Expand All @@ -9,10 +9,9 @@ import {
DEFAULT_CONFIG,
} from "./constants.js";
import { SvelteStoreAdapter } from "./adapter/svelte/store.js";
import { FileHandler } from "./formatHandlers/fileHandler.js";
import { LoadingException } from "./formatHandlers/exception.js";

const HANDLERS = [YamlHandler, JsonHandler];

/**
* @typedef {{
* translationsDir: string,
Expand All @@ -27,83 +26,32 @@ const HANDLERS = [YamlHandler, JsonHandler];
*/
export function t18s(userConfig = {}) {
const logger = new Logger();
const Adapter = new SvelteStoreAdapter();
const adapter = new SvelteStoreAdapter();
const fileHandler = new FileHandler([YamlHandler, JsonHandler]);

/** @type {import("./types.js").ResolvedPluginConfig} */
let config;

/** @type {import("./types.js").LocaleDictionaries} */
const localeDictionaries = new Map();

/**
* Gets the correct handler for a file, using it's file extension.
* Logs error messages if no handler could be found.
*
* @param {string} filePath
* @returns {import("./formatHandlers/types.js").FormatHandler | null}
*/
function getHandler(filePath) {
const filename = basename(filePath);
const fileExtension = filename.split(".").at(-1);

if (!fileExtension) {
logger.error(`Could not determine file extension for ${filePath}`);
return null;
}

const handler = HANDLERS.find((l) =>
l.fileExtensions.includes(fileExtension),
);

if (!handler) {
logger.warn(
`Could not find translation handler for .${fileExtension} files. Ignoring file ${filePath}`,
);
return null;
}

return handler;
}

/**
* Register a new translation file.
* @param {string} filePath Absolute path to the file that needs to be invalidated
*/
async function addTranslationFile(filePath) {
const handler = getHandler(filePath);
if (!handler) return;

const filename = basename(filePath);
const fileExtension = filename.split(".").at(-1);
const locale = filename.split(".")[0];
const locale = getLocale(filePath);

if (!fileExtension) {
logger.error(`Could not determine file extension for ${filePath}`);
return;
}

//Attempt to read the file
let textContent = "";
try {
textContent = await readFile(filePath, "utf-8");
} catch (e) {
logger.error(`Could not read file ${filePath}`);
return;
}

/** @type {import("./types.js").Dictionary} */
let dictionary;
try {
dictionary = await handler.load(filePath, textContent, locale);
const dictionary = await fileHandler.handle(filePath, locale);
localeDictionaries.set(locale, dictionary);
} catch (e) {
if (!(e instanceof LoadingException)) throw e;
logger.error(e.message);
}

localeDictionaries.set(locale, dictionary);

await regenerateDTS();
Adapter.HMRAddLocale(locale);
adapter.HMRAddLocale(locale);
}

/**
Expand All @@ -113,52 +61,39 @@ export function t18s(userConfig = {}) {
* @param {string} filePath Absolute path to the file that needs to be invalidated
*/
async function invalidateTranslationFile(filePath) {
const handler = getHandler(filePath);
if (!handler) return;

const filename = basename(filePath);
const fileExtension = filename.split(".").at(-1);
const locale = filename.split(".")[0];

if (!fileExtension) {
logger.error(`Could not determine file extension for ${filePath}`);
return;
}
const textContent = await readFile(filePath, "utf-8");
const locale = getLocale(filePath);

/** @type {import("./types.js").Dictionary} */
let dictionary;
try {
dictionary = await handler.load(filePath, textContent, locale);
const dictionary = await fileHandler.handle(filePath, locale);
localeDictionaries.set(locale, dictionary);
} catch (e) {
if (!(e instanceof LoadingException)) throw e;
logger.error(e.message);
}

localeDictionaries.set(locale, dictionary);

await regenerateDTS();
Adapter.HMRInvalidateLocale(locale);
adapter.HMRInvalidateLocale(locale);
}

/**
* Remove a _translation_ file.
* Assumes the file is in the `translationsDir` directory.
* @param {string} file Absolute path to the translation file that no longer exists
* @param {string} filePath Absolute path to the translation file that no longer exists
* @returns void
*/
async function removeTranslationFile(file) {
const filename = basename(file);
async function removeTranslationFile(filePath) {
const filename = basename(filePath);
const locale = filename.split(".")[0];

if (!locale) throw new Error("Could not determine locale for ${filePath}");
localeDictionaries.delete(locale);

await regenerateDTS();
Adapter.HMRRemoveLocale(locale);
adapter.HMRRemoveLocale(locale);
}

async function regenerateDTS() {
const dts = Adapter.getTypeDefinition(localeDictionaries);
const dts = adapter.getTypeDefinition(localeDictionaries);
await writeFile(config.dtsPath, dts, { encoding: "utf-8" });
}

Expand Down Expand Up @@ -186,6 +121,20 @@ export function t18s(userConfig = {}) {
*/
const isTranslationFile = (path) => path.startsWith(config.translationsDir);

/**
* Resolves the locale a given path belongs to.
* @param {string} path
* @returns {string}
*
* @throws {Error} If the path does not belong to any locale
*/
const getLocale = (path) => {
const filename = basename(path);
const locale = filename.split(".")[0];
if (!locale) throw new Error("Could not determine locale for ${filePath}");
return locale;
};

return {
name: "t18s",
enforce: "pre",
Expand Down Expand Up @@ -215,15 +164,16 @@ export function t18s(userConfig = {}) {
},

load(id) {
id = id.split("?")[0]; //Remove query parameters
id = id.split("?")[0] ?? ""; //Remove query parameters
if (!id.startsWith(RESOLVED_VIRTUAL_MODULE_PREFIX)) return;

if (id === RESOLVED_VIRTUAL_MODULE_PREFIX) {
return Adapter.getMainCode(localeDictionaries);
return adapter.getMainCode(localeDictionaries);
}

const locale = id.split("/")[2];
return Adapter.getDictionaryCode(
if (!locale) return;
return adapter.getDictionaryCode(
localeDictionaries.get(locale) || new Map(),
);
},
Expand All @@ -244,7 +194,7 @@ export function t18s(userConfig = {}) {
await invalidateTranslationFile(path);
});

Adapter.useServer(server);
adapter.useServer(server);
},
};
}
5 changes: 4 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
"target": "ESNext",
"noEmit": true,
"moduleResolution": "NodeNext",
"module": "NodeNext"
"module": "NodeNext",
"noImplicitAny": true,
"noUncheckedIndexedAccess": true,
"strict": true
},
"include": ["src"]
}

0 comments on commit a2f6646

Please sign in to comment.