@ox-content/vite-plugin 0.13.0 → 0.15.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/index.js CHANGED
@@ -11003,6 +11003,251 @@ function createOgViewerPlugin(options) {
11003
11003
  };
11004
11004
  }
11005
11005
 
11006
+ //#endregion
11007
+ //#region src/i18n.ts
11008
+ /**
11009
+ * i18n plugin for Ox Content.
11010
+ *
11011
+ * Provides:
11012
+ * - Dictionary loading and validation at build time
11013
+ * - Virtual module for i18n config
11014
+ * - Build-time i18n checking
11015
+ * - Locale-aware routing middleware for dev server
11016
+ */
11017
+ /**
11018
+ * Resolves i18n options with defaults.
11019
+ */
11020
+ function resolveI18nOptions(options) {
11021
+ if (options === false) return false;
11022
+ if (!options || !options.enabled) return false;
11023
+ const defaultLocale = options.defaultLocale ?? "en";
11024
+ const locales = options.locales ?? [{
11025
+ code: defaultLocale,
11026
+ name: defaultLocale
11027
+ }];
11028
+ if (!locales.some((l) => l.code === defaultLocale)) locales.unshift({
11029
+ code: defaultLocale,
11030
+ name: defaultLocale
11031
+ });
11032
+ return {
11033
+ enabled: true,
11034
+ dir: options.dir ?? "content/i18n",
11035
+ defaultLocale,
11036
+ locales,
11037
+ hideDefaultLocale: options.hideDefaultLocale ?? true,
11038
+ check: options.check ?? true,
11039
+ functionNames: options.functionNames ?? ["t", "$t"]
11040
+ };
11041
+ }
11042
+ /**
11043
+ * Creates the i18n sub-plugin for the Vite plugin array.
11044
+ */
11045
+ function createI18nPlugin(resolvedOptions) {
11046
+ const i18nOptions = resolvedOptions.i18n;
11047
+ let root = process.cwd();
11048
+ return {
11049
+ name: "ox-content:i18n",
11050
+ configResolved(config) {
11051
+ root = config.root;
11052
+ },
11053
+ resolveId(id) {
11054
+ if (id === "virtual:ox-content/i18n") return "\0virtual:ox-content/i18n";
11055
+ return null;
11056
+ },
11057
+ load(id) {
11058
+ if (id === "\0virtual:ox-content/i18n") {
11059
+ if (!i18nOptions) return `export const i18n = { enabled: false }; export default i18n;`;
11060
+ return generateI18nModule(i18nOptions, root);
11061
+ }
11062
+ return null;
11063
+ },
11064
+ async buildStart() {
11065
+ if (!i18nOptions || !i18nOptions.check) return;
11066
+ const dictDir = path.resolve(root, i18nOptions.dir);
11067
+ if (!fs$1.existsSync(dictDir)) {
11068
+ console.warn(`[ox-content:i18n] Dictionary directory not found: ${dictDir}`);
11069
+ return;
11070
+ }
11071
+ try {
11072
+ const { loadDictionaries, checkI18n, extractTranslationKeys } = await import("@ox-content/napi");
11073
+ const loadResult = loadDictionaries(dictDir);
11074
+ if (loadResult.errors.length > 0) {
11075
+ for (const error of loadResult.errors) console.warn(`[ox-content:i18n] ${error}`);
11076
+ return;
11077
+ }
11078
+ console.log(`[ox-content:i18n] Loaded ${loadResult.localeCount} locales: ${loadResult.locales.join(", ")}`);
11079
+ const checkResult = checkI18n(dictDir, collectKeysFromSource(root, extractTranslationKeys, i18nOptions));
11080
+ if (checkResult.errorCount > 0 || checkResult.warningCount > 0) {
11081
+ for (const diag of checkResult.diagnostics) if (diag.severity === "error") console.error(`[ox-content:i18n] ${diag.message}`);
11082
+ else if (diag.severity === "warning") console.warn(`[ox-content:i18n] ${diag.message}`);
11083
+ }
11084
+ } catch {}
11085
+ },
11086
+ configureServer(server) {
11087
+ if (!i18nOptions) return;
11088
+ const dictDir = path.resolve(root, i18nOptions.dir);
11089
+ if (fs$1.existsSync(dictDir)) {
11090
+ server.watcher.add(dictDir);
11091
+ server.watcher.on("change", (filePath) => {
11092
+ if (!filePath.startsWith(dictDir)) return;
11093
+ if (!/\.(json|yaml|yml)$/.test(filePath)) return;
11094
+ const mod = server.moduleGraph.getModuleById("\0virtual:ox-content/i18n");
11095
+ if (mod) server.moduleGraph.invalidateModule(mod);
11096
+ server.ws.send({ type: "full-reload" });
11097
+ });
11098
+ }
11099
+ server.middlewares.use((req, _res, next) => {
11100
+ if (!req.url) return next();
11101
+ const localeMatch = req.url.match(/^\/([a-z]{2}(?:-[a-zA-Z]+)?)(\/|$)/);
11102
+ if (localeMatch) {
11103
+ const localeCode = localeMatch[1];
11104
+ if (i18nOptions.locales.some((l) => l.code === localeCode)) req.__oxLocale = localeCode;
11105
+ } else if (i18nOptions.hideDefaultLocale) req.__oxLocale = i18nOptions.defaultLocale;
11106
+ next();
11107
+ });
11108
+ }
11109
+ };
11110
+ }
11111
+ /**
11112
+ * Generates the virtual module for i18n configuration.
11113
+ */
11114
+ function generateI18nModule(options, root) {
11115
+ const dictDir = path.resolve(root, options.dir);
11116
+ const localesJson = JSON.stringify(options.locales);
11117
+ const defaultLocale = JSON.stringify(options.defaultLocale);
11118
+ let dictionariesCode = "{}";
11119
+ try {
11120
+ const napi = __require("@ox-content/napi");
11121
+ if (napi.loadDictionariesFlat) {
11122
+ const dictData = napi.loadDictionariesFlat(dictDir);
11123
+ dictionariesCode = JSON.stringify(dictData);
11124
+ } else dictionariesCode = JSON.stringify(loadDictionariesFallback(options, dictDir));
11125
+ } catch {
11126
+ try {
11127
+ dictionariesCode = JSON.stringify(loadDictionariesFallback(options, dictDir));
11128
+ } catch {}
11129
+ }
11130
+ return `
11131
+ export const i18nConfig = {
11132
+ enabled: true,
11133
+ defaultLocale: ${defaultLocale},
11134
+ locales: ${localesJson},
11135
+ hideDefaultLocale: ${JSON.stringify(options.hideDefaultLocale)},
11136
+ };
11137
+
11138
+ export const dictionaries = ${dictionariesCode};
11139
+
11140
+ export function t(key, params, locale) {
11141
+ const dict = dictionaries[locale || i18nConfig.defaultLocale] || {};
11142
+ let message = dict[key];
11143
+ if (!message) {
11144
+ const fallback = dictionaries[i18nConfig.defaultLocale] || {};
11145
+ message = fallback[key] || key;
11146
+ }
11147
+ if (params) {
11148
+ for (const [k, v] of Object.entries(params)) {
11149
+ message = message.replace(new RegExp('\\\\{\\\\$' + k + '\\\\}', 'g'), String(v));
11150
+ }
11151
+ }
11152
+ return message;
11153
+ }
11154
+
11155
+ export function getLocaleFromPath(pathname) {
11156
+ const match = pathname.match(/^\\/([a-z]{2}(?:-[a-zA-Z]+)?)(\\//|$)/);
11157
+ if (match) {
11158
+ const code = match[1];
11159
+ if (i18nConfig.locales.some(l => l.code === code)) {
11160
+ return code;
11161
+ }
11162
+ }
11163
+ return i18nConfig.defaultLocale;
11164
+ }
11165
+
11166
+ export function localePath(pathname, locale) {
11167
+ const current = getLocaleFromPath(pathname);
11168
+ let clean = pathname;
11169
+ if (current !== i18nConfig.defaultLocale || !i18nConfig.hideDefaultLocale) {
11170
+ clean = pathname.replace(new RegExp('^/' + current + '(/|$)'), '/');
11171
+ }
11172
+ if (locale === i18nConfig.defaultLocale && i18nConfig.hideDefaultLocale) {
11173
+ return clean || '/';
11174
+ }
11175
+ return '/' + locale + (clean.startsWith('/') ? clean : '/' + clean);
11176
+ }
11177
+
11178
+ export default { i18nConfig, dictionaries, t, getLocaleFromPath, localePath };
11179
+ `;
11180
+ }
11181
+ /**
11182
+ * Flattens a nested object into dot-separated keys.
11183
+ */
11184
+ function flattenObject(obj, prefix, result) {
11185
+ for (const [key, value] of Object.entries(obj)) {
11186
+ const fullKey = `${prefix}.${key}`;
11187
+ if (typeof value === "string") result[fullKey] = value;
11188
+ else if (typeof value === "object" && value !== null && !Array.isArray(value)) flattenObject(value, fullKey, result);
11189
+ else result[fullKey] = String(value);
11190
+ }
11191
+ }
11192
+ /**
11193
+ * Fallback dictionary loading using TS-based JSON file reading.
11194
+ */
11195
+ function loadDictionariesFallback(options, dictDir) {
11196
+ const dictData = {};
11197
+ for (const locale of options.locales) {
11198
+ const localeDir = path.join(dictDir, locale.code);
11199
+ if (!fs$1.existsSync(localeDir)) continue;
11200
+ const files = fs$1.readdirSync(localeDir);
11201
+ const localeDict = {};
11202
+ for (const file of files) {
11203
+ if (!file.endsWith(".json")) continue;
11204
+ const filePath = path.join(localeDir, file);
11205
+ const content = fs$1.readFileSync(filePath, "utf-8");
11206
+ const namespace = path.basename(file, ".json");
11207
+ try {
11208
+ flattenObject(JSON.parse(content), namespace, localeDict);
11209
+ } catch {}
11210
+ }
11211
+ dictData[locale.code] = localeDict;
11212
+ }
11213
+ return dictData;
11214
+ }
11215
+ /**
11216
+ * Collects translation keys from source files using NAPI extractTranslationKeys.
11217
+ */
11218
+ function collectKeysFromSource(root, extractTranslationKeys, options) {
11219
+ const srcDir = path.resolve(root, "src");
11220
+ const keys = /* @__PURE__ */ new Set();
11221
+ if (fs$1.existsSync(srcDir)) walkDir(srcDir, /\.(ts|tsx|js|jsx)$/, (filePath) => {
11222
+ const usages = extractTranslationKeys(fs$1.readFileSync(filePath, "utf-8"), filePath, options.functionNames);
11223
+ for (const usage of usages) keys.add(usage.key);
11224
+ });
11225
+ const contentDir = path.resolve(root, "content");
11226
+ if (fs$1.existsSync(contentDir)) {
11227
+ const tPattern = /\{\{t\(['"]([^'"]+)['"]\)\}\}/g;
11228
+ walkDir(contentDir, /\.(md|mdx)$/, (filePath) => {
11229
+ const content = fs$1.readFileSync(filePath, "utf-8");
11230
+ let match;
11231
+ while ((match = tPattern.exec(content)) !== null) keys.add(match[1]);
11232
+ tPattern.lastIndex = 0;
11233
+ });
11234
+ }
11235
+ return Array.from(keys);
11236
+ }
11237
+ /**
11238
+ * Recursively walks a directory, calling the callback for files matching the pattern.
11239
+ */
11240
+ function walkDir(dir, pattern, callback) {
11241
+ const entries = fs$1.readdirSync(dir, { withFileTypes: true });
11242
+ for (const entry of entries) {
11243
+ const fullPath = path.join(dir, entry.name);
11244
+ if (entry.isDirectory()) {
11245
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
11246
+ walkDir(fullPath, pattern, callback);
11247
+ } else if (pattern.test(entry.name)) callback(fullPath);
11248
+ }
11249
+ }
11250
+
11006
11251
  //#endregion
11007
11252
  //#region src/jsx-runtime.ts
11008
11253
  /**
@@ -11729,6 +11974,7 @@ function oxContent(options = {}) {
11729
11974
  }
11730
11975
  }
11731
11976
  ];
11977
+ if (resolvedOptions.i18n) plugins.push(createI18nPlugin(resolvedOptions));
11732
11978
  if (resolvedOptions.ogViewer) plugins.push(createOgViewerPlugin(resolvedOptions));
11733
11979
  return plugins;
11734
11980
  }
@@ -11758,7 +12004,8 @@ function resolveOptions(options) {
11758
12004
  transformers: options.transformers ?? [],
11759
12005
  docs: resolveDocsOptions(options.docs),
11760
12006
  search: resolveSearchOptions(options.search),
11761
- ogViewer: options.ogViewer ?? true
12007
+ ogViewer: options.ogViewer ?? true,
12008
+ i18n: resolveI18nOptions(options.i18n)
11762
12009
  };
11763
12010
  }
11764
12011
  /**
@@ -11780,5 +12027,5 @@ function generateVirtualModule(path, options) {
11780
12027
  }
11781
12028
 
11782
12029
  //#endregion
11783
- export { DEFAULT_HTML_TEMPLATE, DefaultTheme, Fragment, buildSearchIndex, buildSsg, clearRenderContext, collectGitHubRepos, collectOgpUrls, createMarkdownEnvironment, createTheme, defaultTheme, defineTheme, each, extractDocs, extractIslandInfo, extractVideoId, fetchOgpData, fetchRepoData, generateFrontmatterTypes, generateHydrationScript, generateMarkdown, generateOgImages, generateTabsCSS, generateTypes, hasIslands, inferType, jsx, jsxs, mergeThemes, mermaidClientScript, oxContent, prefetchGitHubRepos, prefetchOgpData, raw, renderAllPages, renderPage, renderToString, resolveDocsOptions, resolveOgImageOptions, resolveSearchOptions, resolveSsgOptions, resolveTheme, setRenderContext, transformAllPlugins, transformGitHub, transformIslands, transformMarkdown, transformMermaidStatic, transformOgp, transformTabs, transformYouTube, useIsActive, useNav, usePageProps, useRenderContext, useSiteConfig, when, writeDocs, writeSearchIndex };
12030
+ export { DEFAULT_HTML_TEMPLATE, DefaultTheme, Fragment, buildSearchIndex, buildSsg, clearRenderContext, collectGitHubRepos, collectOgpUrls, createI18nPlugin, createMarkdownEnvironment, createTheme, defaultTheme, defineTheme, each, extractDocs, extractIslandInfo, extractVideoId, fetchOgpData, fetchRepoData, generateFrontmatterTypes, generateHydrationScript, generateMarkdown, generateOgImages, generateTabsCSS, generateTypes, hasIslands, inferType, jsx, jsxs, mergeThemes, mermaidClientScript, oxContent, prefetchGitHubRepos, prefetchOgpData, raw, renderAllPages, renderPage, renderToString, resolveDocsOptions, resolveI18nOptions, resolveOgImageOptions, resolveSearchOptions, resolveSsgOptions, resolveTheme, setRenderContext, transformAllPlugins, transformGitHub, transformIslands, transformMarkdown, transformMermaidStatic, transformOgp, transformTabs, transformYouTube, useIsActive, useNav, usePageProps, useRenderContext, useSiteConfig, when, writeDocs, writeSearchIndex };
11784
12031
  //# sourceMappingURL=index.js.map