Skip to content

Commit

Permalink
Add new routingStrategy API (#51)
Browse files Browse the repository at this point in the history
* Improve .gitignore configuration for examples

* [i18nIgnore] Update VitePress example structure

* Add new `routingStrategy` API

* Remove unnecessary comment

* No need to define the default option

* Create tidy-days-learn.md
  • Loading branch information
yanthomasdev authored Nov 22, 2023
1 parent 8fea07a commit f57c9f1
Show file tree
Hide file tree
Showing 11 changed files with 113 additions and 85 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-days-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lunariajs/core": patch
---

Add new `routingStrategy` API
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ node_modules/
# build output
dist/

# examples build output
.vitepress/

# logs
npm-debug.log*
yarn-debug.log*
Expand Down
2 changes: 2 additions & 0 deletions examples/vitepress/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# example build output
.vitepress/cache
21 changes: 21 additions & 0 deletions examples/vitepress/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineConfig } from 'vitepress';

export default defineConfig({
locales: {
en: {
label: 'English',
lang: 'en',
link: '/en/',
},
pt: {
label: 'Português',
lang: 'pt',
link: '/pt/',
},
es: {
label: 'Spanish',
lang: 'es',
link: '/es/',
},
},
});
File renamed without changes.
File renamed without changes.
File renamed without changes.
5 changes: 2 additions & 3 deletions examples/vitepress/lunaria.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
"label": "English",
"lang": "en",
"content": {
"location": "**/*.md",
"ignore": ["pt/*.md", "es/*.md"]
"location": "en/**/*.md"
},
"dictionaries": {
"location": "ui/en/*.{js,cjs,mjs,ts,yml,json}",
Expand Down Expand Up @@ -47,5 +46,5 @@
}
],
"translatableProperty": "i18nReady",
"renderer": "./renderer.config.js"
"renderer": "./renderer.config.ts"
}
30 changes: 25 additions & 5 deletions packages/core/src/schemas/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { normalizeURL } from 'ufo';
import { z } from 'zod';
import { DashboardSchema } from '../schemas/dashboard.js';
import { LocaleSchema } from '../schemas/locale.js';
import { LocalePathConstructorSchema, SharedPathResolverSchema } from '../schemas/misc.js';
import type { CustomComponent, CustomStatusComponent } from '../types.js';

function createComponentSchema<ComponentType extends CustomComponent | CustomStatusComponent>() {
Expand All @@ -14,6 +13,22 @@ function createComponentSchema<ComponentType extends CustomComponent | CustomSta
}, 'Custom components need to be a function returning a valid `lit-html` template.');
}

export const customRoutingStrategyOptionsSchema = z.object({
regex: z
.string()
.describe(
"A regex pattern to find the path section to be replaced. You can use :lunaria-locales to dynamically add a list of all the locales in the format `'es|pt|ar'`."
),
localePathReplaceWith: z
.string()
.describe(
"The content that will be replaced into the `toLocalePath` regex's match. You can use :lunaria-locale to dynamically add the current locale for you to replace with."
),
sharedPathReplaceWith: z
.string()
.describe("The content that will be replaced into the `toSharedPath` regex's match."),
});

export const LunariaConfigSchema = z.object({
/** Options about your generated dashboard. */
dashboard: DashboardSchema,
Expand All @@ -34,10 +49,15 @@ export const LunariaConfigSchema = z.object({
.string()
.optional()
.describe('Name of the frontmatter property used to mark a page as ready for translation.'),
/** Fuction to extract a shared path from a locale's path, used to 'link' the content between two locales. */
sharedPathResolver: SharedPathResolverSchema,
/** Fuction to construct the locale-specific path from the source path of the same content. */
localePathConstructor: LocalePathConstructorSchema,
/** The routing strategy used by your framework, used to properly generate paths from a locale's path. */
routingStrategy: z
.literal('directory')
.or(z.literal('file'))
.or(customRoutingStrategyOptionsSchema)
.default('directory')
.describe(
"The routing strategy used by your framework, used to properly generate paths from a locale's path."
),
/** The URL of your current repository, used to generate history links, e.g. `"https://github.com/Yan-Thomas/lunaria/"`. */
repository: z
.string()
Expand Down
57 changes: 0 additions & 57 deletions packages/core/src/schemas/misc.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,7 @@
import { z } from 'zod';
import type { DictionaryObject } from '../types.js';

export const SharedPathResolverSchema = z
.function()
.args(
z.object({
lang: z.string(),
localePath: z.string(),
})
)
.returns(z.string())
.optional()
.default(() => ({ lang, localePath }: { lang: string; localePath: string }) => {
const pathParts = localePath.split('/');
const localePartIndex = pathParts.findIndex((part) => part === lang);
if (localePartIndex > -1) pathParts.splice(localePartIndex, 1);

return pathParts.join('/');
})
.describe(
"Fuction to extract a shared path from a locale's path, used to 'link' the content between two locales."
);

export const LocalePathConstructorSchema = z
.function()
.args(
z.object({
sourceLang: z.string(),
localeLang: z.string(),
sourcePath: z.string(),
})
)
.returns(z.string())
.optional()
.default(
() =>
({
sourceLang,
localeLang,
sourcePath,
}: {
sourceLang: string;
localeLang: string;
sourcePath: string;
}) => {
const pathParts = sourcePath.split('/');
const localePartIndex = pathParts.findIndex((part) => part === sourceLang);
if (localePartIndex > -1) pathParts.splice(localePartIndex, 1, localeLang);

return pathParts.join('/');
}
)
.describe(
'Fuction to construct the locale-specific path from the source path of the same content.'
);

export const DictionaryContentSchema: z.ZodType<DictionaryObject> = z.record(
z.string(),
z.lazy(() => z.string().or(DictionaryContentSchema))
);

export type SharedPathResolver = z.infer<typeof SharedPathResolverSchema>;
export type LocalePathConstructor = z.infer<typeof LocalePathConstructorSchema>;
75 changes: 58 additions & 17 deletions packages/core/src/tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ export async function getTranslationStatus(
opts: LunariaConfig,
fileContentIndex: FileContentIndex
) {
const { defaultLocale, locales, repository, rootDir, localePathConstructor } = opts;
const { defaultLocale, locales, repository, rootDir, routingStrategy } = opts;

const sourceLocaleIndex = fileContentIndex[defaultLocale.lang];
const translationStatus: FileTranslationStatus[] = [];

const allLangs = [defaultLocale, ...locales].map((locale) => locale.lang);

if (!sourceLocaleIndex) {
console.error(
new Error(
Expand Down Expand Up @@ -69,16 +72,15 @@ export async function getTranslationStatus(
locales.map(async ({ lang }) => {
const translationFile = fileContentIndex[lang]?.[sharedPath];

const localeFilePath = localePathConstructor({
localeLang: lang,
sourceLang: defaultLocale.lang,
sourcePath: sourceFile.filePath,
});
const localeFilePath = getPathResolver(routingStrategy, allLangs).toLocalePath(
sourceFile.filePath,
lang
);

const existingPageURL = getGitHubURL({
repository: repository,
rootDir: rootDir,
filePath: localeFilePath,
filePath: translationFile?.filePath,
});

const missingPageURL = getGitHubURL({
Expand Down Expand Up @@ -118,17 +120,14 @@ export async function getTranslationStatus(
}

export async function getContentIndex(opts: LunariaConfig, isShallowRepo: boolean) {
const {
translatableProperty,
rootDir,
defaultLocale,
locales,
ignoreKeywords,
sharedPathResolver,
} = opts;
const { translatableProperty, rootDir, defaultLocale, locales, ignoreKeywords, routingStrategy } =
opts;

const allLocales = [defaultLocale, ...locales];
const allLangs = allLocales.map((locale) => locale.lang);

const fileContentIndex: FileContentIndex = {};
const pathResolver = getPathResolver(routingStrategy, allLangs);

for (const { lang, content, dictionaries } of allLocales) {
const localeContentPaths = await glob(content.location, {
Expand All @@ -138,7 +137,7 @@ export async function getContentIndex(opts: LunariaConfig, isShallowRepo: boolea

const genericContentIndex = await Promise.all(
localeContentPaths.sort().map(async (filePath) => {
const sharedPath = sharedPathResolver({ lang, localePath: filePath });
const sharedPath = pathResolver.toSharedPath(filePath);

return {
lang,
Expand Down Expand Up @@ -171,7 +170,7 @@ export async function getContentIndex(opts: LunariaConfig, isShallowRepo: boolea
dictionaryContentIndex.push(
...(await Promise.all(
localeDictionariesPaths.sort().map(async (filePath) => {
const sharedPath = sharedPathResolver({ lang: lang, localePath: filePath });
const sharedPath = pathResolver.toSharedPath(filePath);
// Create or update page data for the page
return {
lang,
Expand Down Expand Up @@ -264,6 +263,48 @@ async function getFileData(
};
}

export function getPathResolver(
routingStrategy: LunariaConfig['routingStrategy'],
allLangs: string[]
) {
if (routingStrategy === 'directory') {
const directoryRegExp = getRegexWithVariable('(:lunaria-locales)/', allLangs);

return {
toLocalePath: (path: string, lang: string) => path.replace(directoryRegExp, `${lang}/`),
toSharedPath: (path: string) => path.replace(directoryRegExp, ''),
};
}

/** TODO: Test this with Nextra to see if it's 100% compatible. */
if (routingStrategy === 'file') {
const fileRegExp = getRegexWithVariable('.(:lunaria-locales).', allLangs);

return {
toLocalePath: (path: string, lang: string) => path.replace(fileRegExp, `.${lang}.`),
toSharedPath: (path: string) => path.replace(fileRegExp, ''),
};
}

const { regex, sharedPathReplaceWith, localePathReplaceWith } = routingStrategy;

const customLocalePathRegExp = getRegexWithVariable(regex, allLangs);
const customSharedPathRegExp = getRegexWithVariable(regex, allLangs);

return {
toLocalePath: (path: string, lang: string) =>
path.replace(customLocalePathRegExp, localePathReplaceWith.replace(':lunaria-locale', lang)),
toSharedPath: (path: string) => path.replace(customSharedPathRegExp, sharedPathReplaceWith),
};
}

function getRegexWithVariable(regex: string, allLangs: string[]) {
const allLangsRegexPartial = allLangs.join('|');
const regexStringWithVariable = regex.replaceAll(':lunaria-locales', allLangsRegexPartial);

return new RegExp(regexStringWithVariable);
}

function findLastMajorCommit(
filePath: string,
allCommits: readonly (DefaultLogFields & ListLogLine)[],
Expand Down

0 comments on commit f57c9f1

Please sign in to comment.