Add internationalization support with astro-i18next integration
- Implemented astro-i18next for multi-language support, including English, Dutch, and Italian. - Configured default locale and language fallback settings. - Defined routes for localized content in the configuration. - Updated package.json and package-lock.json to include new dependencies for i18next and related plugins.
This commit is contained in:
94
node_modules/astro-i18next/src/cli/generate.ts
generated
vendored
Normal file
94
node_modules/astro-i18next/src/cli/generate.ts
generated
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
import fs from "fs";
|
||||
import { resolve } from "pathe";
|
||||
import { AstroI18nextConfig } from "../types";
|
||||
import {
|
||||
getAstroPagesFullPaths,
|
||||
createFiles,
|
||||
FileToGenerate,
|
||||
generateLocalizedFrontmatter,
|
||||
overwriteAstroFrontmatter,
|
||||
parseFrontmatter,
|
||||
resolveTranslatedAstroPath,
|
||||
resolveRelativePathsLevel,
|
||||
} from "./utils";
|
||||
|
||||
/**
|
||||
* Reads all files inside inputPath
|
||||
*
|
||||
* @param inputPath
|
||||
* @param locales
|
||||
* @param outputPath
|
||||
*/
|
||||
export const generate = (
|
||||
inputPath: string,
|
||||
defaultLocale: AstroI18nextConfig["defaultLocale"],
|
||||
locales: AstroI18nextConfig["locales"],
|
||||
showDefaultLocale = false,
|
||||
flatRoutes?: AstroI18nextConfig["flatRoutes"],
|
||||
outputPath: string = inputPath
|
||||
): { filesToGenerate: FileToGenerate[]; timeToProcess: number } => {
|
||||
const start = process.hrtime();
|
||||
|
||||
// default locale page paths
|
||||
const astroPagesFullPaths = showDefaultLocale
|
||||
? getAstroPagesFullPaths(inputPath, defaultLocale, locales)
|
||||
: getAstroPagesFullPaths(inputPath, undefined, locales);
|
||||
|
||||
const filesToGenerate: FileToGenerate[] = [];
|
||||
astroPagesFullPaths.forEach(async function (astroFileFullPath: string) {
|
||||
const astroFilePath = resolve(astroFileFullPath).replace(
|
||||
resolve(inputPath),
|
||||
""
|
||||
);
|
||||
|
||||
const inputFilePath = showDefaultLocale
|
||||
? [inputPath, defaultLocale, astroFilePath].join("/")
|
||||
: [inputPath, astroFilePath].join("/");
|
||||
|
||||
const fileContents = fs.readFileSync(inputFilePath);
|
||||
const fileContentsString = fileContents.toString();
|
||||
|
||||
const parsedFrontmatter = parseFrontmatter(fileContentsString);
|
||||
|
||||
locales.forEach((locale) => {
|
||||
const isOtherLocale = locale !== defaultLocale;
|
||||
const fileDepth = showDefaultLocale ? 0 : Number(isOtherLocale);
|
||||
|
||||
// add i18next's changeLanguage function to frontmatter
|
||||
const frontmatterCode = generateLocalizedFrontmatter(
|
||||
parsedFrontmatter,
|
||||
locale
|
||||
);
|
||||
|
||||
// get the astro file contents
|
||||
let newFileContents = overwriteAstroFrontmatter(
|
||||
fileContentsString,
|
||||
frontmatterCode
|
||||
);
|
||||
|
||||
// add depth to imports and Astro.glob pattern
|
||||
newFileContents = resolveRelativePathsLevel(newFileContents, fileDepth);
|
||||
|
||||
const createLocaleFolder = showDefaultLocale ? true : isOtherLocale;
|
||||
|
||||
filesToGenerate.push({
|
||||
path: resolve(
|
||||
resolveTranslatedAstroPath(
|
||||
astroFilePath,
|
||||
createLocaleFolder ? locale : undefined,
|
||||
outputPath,
|
||||
flatRoutes
|
||||
)
|
||||
),
|
||||
source: newFileContents,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
createFiles(filesToGenerate);
|
||||
|
||||
return {
|
||||
filesToGenerate,
|
||||
timeToProcess: process.hrtime(start)[1] / 1000000,
|
||||
};
|
||||
};
|
74
node_modules/astro-i18next/src/cli/index.ts
generated
vendored
Executable file
74
node_modules/astro-i18next/src/cli/index.ts
generated
vendored
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { flattenRoutes } from "../config";
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
import { generate } from "./generate";
|
||||
import { loadConfig, normalizePath } from "./middlewares";
|
||||
import { GenerateArgs } from "./types";
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.usage("usage: $0 <command>")
|
||||
.command<GenerateArgs>(
|
||||
"generate [path] [options]",
|
||||
"generates localized Astro pages",
|
||||
(yargs) => {
|
||||
return yargs
|
||||
.positional("path", {
|
||||
type: "string",
|
||||
description: "Path to the Astro project folder",
|
||||
default: "./",
|
||||
})
|
||||
.option("output", {
|
||||
alias: "o",
|
||||
type: "string",
|
||||
description:
|
||||
"Set the output of the generated pages if different from input",
|
||||
});
|
||||
},
|
||||
async (argv) => {
|
||||
if (argv.verbose) {
|
||||
console.info(`Generating localized pages: ${argv.config.locales}`);
|
||||
}
|
||||
|
||||
const pagesPath = argv.path + "src/pages";
|
||||
|
||||
const flatRoutes = flattenRoutes(argv.config.routes);
|
||||
|
||||
const result = generate(
|
||||
pagesPath,
|
||||
argv.config.defaultLocale,
|
||||
argv.config.locales,
|
||||
argv.config.showDefaultLocale,
|
||||
flatRoutes,
|
||||
argv.output
|
||||
);
|
||||
|
||||
if (argv.verbose) {
|
||||
const filepaths = result.filesToGenerate.map(
|
||||
(fileToGenerate) => fileToGenerate.path
|
||||
);
|
||||
console.log(`\n✨ ${filepaths.join("\n✨ ")}\n`);
|
||||
}
|
||||
|
||||
// All good! Show success feedback
|
||||
console.log(
|
||||
`🧪 Localized .astro pages were generated successfully, it took ${result.timeToProcess.toFixed()}ms!`
|
||||
);
|
||||
}
|
||||
)
|
||||
.middleware([normalizePath, loadConfig], true)
|
||||
.options({
|
||||
config: {
|
||||
alias: "c",
|
||||
type: "string",
|
||||
description:
|
||||
"Set the output of the generated pages if different from input",
|
||||
},
|
||||
verbose: {
|
||||
alias: "v",
|
||||
type: "boolean",
|
||||
description: "Run with verbose logging",
|
||||
},
|
||||
})
|
||||
.parse();
|
32
node_modules/astro-i18next/src/cli/middlewares.ts
generated
vendored
Normal file
32
node_modules/astro-i18next/src/cli/middlewares.ts
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
import { pathToFileURL } from "url";
|
||||
import { MiddlewareFunction } from "yargs";
|
||||
import { getUserConfig } from "../utils";
|
||||
import { GenerateArgs, GlobalArgs } from "./types";
|
||||
|
||||
// @ts-ignore
|
||||
export const loadConfig: MiddlewareFunction<GlobalArgs & GenerateArgs> = async (
|
||||
argv
|
||||
): Promise<GlobalArgs & GenerateArgs> => {
|
||||
const { path, config } = argv;
|
||||
|
||||
const userConfig = await getUserConfig(pathToFileURL(path), config as any);
|
||||
|
||||
if (path && !userConfig?.value) {
|
||||
throw new Error(
|
||||
`Could not find a config file at ${JSON.stringify(
|
||||
path
|
||||
)}. Does the file exist?`
|
||||
);
|
||||
}
|
||||
|
||||
return { ...argv, config: userConfig?.value };
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
export const normalizePath: MiddlewareFunction<
|
||||
GlobalArgs & GenerateArgs
|
||||
> = async (argv): Promise<GlobalArgs & GenerateArgs> => {
|
||||
const { path } = argv;
|
||||
|
||||
return { ...argv, path: path.endsWith("/") ? path : path + "/" };
|
||||
};
|
162
node_modules/astro-i18next/src/cli/transformer.ts
generated
vendored
Normal file
162
node_modules/astro-i18next/src/cli/transformer.ts
generated
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
import ts from "typescript";
|
||||
|
||||
/**
|
||||
* Traverse ts' AST to inject i18next's language switch
|
||||
* @param context
|
||||
* @returns
|
||||
*/
|
||||
export const transformer: ts.TransformerFactory<ts.SourceFile> =
|
||||
(context: ts.TransformationContext) => (rootNode) => {
|
||||
const { factory, getCompilerOptions } = context;
|
||||
let doesI18nextImportExist = false;
|
||||
|
||||
const { locale } = getCompilerOptions();
|
||||
|
||||
function visit(node: ts.Node): ts.Node {
|
||||
// isolate i18next import statement
|
||||
if (
|
||||
ts.isImportDeclaration(node) &&
|
||||
ts.isStringLiteral(node.moduleSpecifier) &&
|
||||
node.moduleSpecifier.text === "i18next" &&
|
||||
ts.isImportClause(node.importClause)
|
||||
) {
|
||||
doesI18nextImportExist = true;
|
||||
|
||||
if (!node.importClause.namedBindings) {
|
||||
return factory.updateImportDeclaration(
|
||||
node,
|
||||
node.modifiers,
|
||||
factory.createImportClause(
|
||||
node.importClause.isTypeOnly,
|
||||
node.importClause.name,
|
||||
factory.createNamedImports([
|
||||
factory.createImportSpecifier(
|
||||
false,
|
||||
undefined,
|
||||
factory.createIdentifier("changeLanguage")
|
||||
),
|
||||
])
|
||||
),
|
||||
node.moduleSpecifier,
|
||||
node.assertClause
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isImportClause(node.importClause) &&
|
||||
ts.isNamedImports(node.importClause.namedBindings) &&
|
||||
!node.importClause.namedBindings.elements.find(
|
||||
(namedImport) => namedImport.name.escapedText === "changeLanguage"
|
||||
)
|
||||
) {
|
||||
// import changeLanguage function in i18next import declaration
|
||||
return factory.updateImportDeclaration(
|
||||
node,
|
||||
node.modifiers,
|
||||
factory.updateImportClause(
|
||||
node.importClause,
|
||||
false,
|
||||
node.importClause.name,
|
||||
factory.updateNamedImports(node.importClause.namedBindings, [
|
||||
...node.importClause.namedBindings.elements,
|
||||
factory.createImportSpecifier(
|
||||
false,
|
||||
undefined,
|
||||
factory.createIdentifier("changeLanguage")
|
||||
),
|
||||
])
|
||||
),
|
||||
node.moduleSpecifier,
|
||||
node.assertClause
|
||||
);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
// remove any occurrence of changeLanguage() call
|
||||
if (
|
||||
ts.isExpressionStatement(node) &&
|
||||
ts.isCallExpression(node.expression) &&
|
||||
((ts.isPropertyAccessExpression(node.expression.expression) &&
|
||||
node.expression.expression.name.escapedText === "changeLanguage") ||
|
||||
(ts.isIdentifier(node.expression.expression) &&
|
||||
node.expression.expression.escapedText === "changeLanguage"))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return ts.visitEachChild(node, visit, context);
|
||||
}
|
||||
|
||||
let visitedNode = ts.visitNode(rootNode, visit);
|
||||
|
||||
const statements = [...visitedNode.statements];
|
||||
|
||||
if (!doesI18nextImportExist) {
|
||||
statements.unshift(
|
||||
factory.createImportDeclaration(
|
||||
undefined,
|
||||
factory.createImportClause(
|
||||
false,
|
||||
undefined,
|
||||
factory.createNamedImports([
|
||||
factory.createImportSpecifier(
|
||||
false,
|
||||
undefined,
|
||||
factory.createIdentifier("changeLanguage")
|
||||
),
|
||||
])
|
||||
),
|
||||
factory.createStringLiteral("i18next"),
|
||||
undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// append the changeLanguage statement after imports
|
||||
|
||||
// get a boolean array with values telling whether or not a statement is an import
|
||||
const importDeclarationsMap = statements.map((statement) =>
|
||||
ts.isImportDeclaration(statement)
|
||||
);
|
||||
|
||||
const lastIndexOfImportDeclaration =
|
||||
importDeclarationsMap.lastIndexOf(true);
|
||||
|
||||
// insert changeLanguage statement after the imports
|
||||
// and surrounded by line breaks
|
||||
statements.splice(
|
||||
lastIndexOfImportDeclaration + 1,
|
||||
0,
|
||||
factory.createIdentifier("\n") as unknown as ts.Statement
|
||||
);
|
||||
statements.splice(
|
||||
lastIndexOfImportDeclaration + 1,
|
||||
0,
|
||||
factory.createExpressionStatement(
|
||||
factory.createCallExpression(
|
||||
factory.createIdentifier("changeLanguage"),
|
||||
undefined,
|
||||
[factory.createStringLiteral(locale as string)]
|
||||
)
|
||||
)
|
||||
);
|
||||
statements.splice(
|
||||
lastIndexOfImportDeclaration + 1,
|
||||
0,
|
||||
factory.createIdentifier("\n") as unknown as ts.Statement
|
||||
);
|
||||
|
||||
visitedNode = factory.updateSourceFile(
|
||||
visitedNode,
|
||||
statements,
|
||||
visitedNode.isDeclarationFile,
|
||||
visitedNode.referencedFiles,
|
||||
visitedNode.typeReferenceDirectives,
|
||||
visitedNode.hasNoDefaultLib,
|
||||
visitedNode.libReferenceDirectives
|
||||
);
|
||||
|
||||
return visitedNode;
|
||||
};
|
11
node_modules/astro-i18next/src/cli/types.ts
generated
vendored
Normal file
11
node_modules/astro-i18next/src/cli/types.ts
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AstroI18nextConfig } from "../types";
|
||||
|
||||
export interface GlobalArgs {
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
export interface GenerateArgs {
|
||||
path: string;
|
||||
config: AstroI18nextConfig;
|
||||
output: string;
|
||||
}
|
182
node_modules/astro-i18next/src/cli/utils.ts
generated
vendored
Normal file
182
node_modules/astro-i18next/src/cli/utils.ts
generated
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
import { fdir, PathsOutput } from "fdir";
|
||||
import fsExtra from "fs-extra";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import ts from "typescript";
|
||||
import { transformer } from "./transformer";
|
||||
import { AstroI18nextConfig } from "../types";
|
||||
|
||||
export interface FileToGenerate {
|
||||
path: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export const doesStringIncludeFrontmatter = (source: string): boolean =>
|
||||
/---.*---/s.test(source);
|
||||
|
||||
export const extractFrontmatterFromAstroSource = (source: string): string => {
|
||||
if (doesStringIncludeFrontmatter(source)) {
|
||||
const {
|
||||
groups: { frontmatter },
|
||||
} = /---(?<frontmatter>(.*))---/s.exec(source);
|
||||
|
||||
return frontmatter;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
export const overwriteAstroFrontmatter = (
|
||||
source: string,
|
||||
frontmatter: string
|
||||
): string => {
|
||||
if (doesStringIncludeFrontmatter(source)) {
|
||||
return source.replace(/---[\s\S]*---/g, `---\n${frontmatter.trim()}\n---`);
|
||||
}
|
||||
|
||||
return `---\n${frontmatter.trim()}\n---\n\n` + source;
|
||||
};
|
||||
|
||||
export const addDepthToRelativePath = (
|
||||
relativePath: string,
|
||||
depth: number = 1
|
||||
): string => {
|
||||
if (relativePath.startsWith("./") && depth > 0) {
|
||||
// remove "./" from relativePath
|
||||
relativePath = relativePath.slice(2);
|
||||
}
|
||||
|
||||
return relativePath.padStart(relativePath.length + depth * 3, "../");
|
||||
};
|
||||
|
||||
/**
|
||||
* file is hidden if its name or any of its parent folders start with an underscore
|
||||
*/
|
||||
export const isFileHidden = (filepath: string): boolean => {
|
||||
return /((^_)|(\/_))/.test(filepath);
|
||||
};
|
||||
|
||||
export const resolveRelativePathsLevel = (
|
||||
fileContents: string,
|
||||
fileDepth: number
|
||||
) => {
|
||||
fileContents = fileContents.replace(
|
||||
/(import\s+.*["'])(\..*)(["'])/g,
|
||||
(_, before, relativePath, after) =>
|
||||
`${before}${addDepthToRelativePath(relativePath, fileDepth)}${after}`
|
||||
);
|
||||
fileContents = fileContents.replace(
|
||||
/(Astro.glob\(["'])(\..*)(["']\))/g,
|
||||
(_, before, relativePath, after) =>
|
||||
`${before}${addDepthToRelativePath(relativePath, fileDepth)}${after}`
|
||||
);
|
||||
fileContents = fileContents.replace(
|
||||
/(<script\s+src=["'])(\..*)(["'])/g,
|
||||
(_, before, relativePath, after) =>
|
||||
`${before}${addDepthToRelativePath(relativePath, fileDepth)}${after}`
|
||||
);
|
||||
|
||||
return fileContents;
|
||||
};
|
||||
|
||||
/* c8 ignore start */
|
||||
/**
|
||||
* parse frontmatter using typescript compiler
|
||||
*
|
||||
* @param source
|
||||
*/
|
||||
export const parseFrontmatter = (source: string): ts.SourceFile =>
|
||||
ts.createSourceFile(
|
||||
"x.ts",
|
||||
extractFrontmatterFromAstroSource(source),
|
||||
ts.ScriptTarget.Latest
|
||||
);
|
||||
|
||||
export const generateLocalizedFrontmatter = (
|
||||
tsNode: ts.SourceFile,
|
||||
locale: string
|
||||
) => {
|
||||
// generate for default locale, then loop over locales to generate other pages
|
||||
const result: ts.TransformationResult<ts.SourceFile> = ts.transform(
|
||||
tsNode,
|
||||
[transformer],
|
||||
{ locale }
|
||||
);
|
||||
const printer = ts.createPrinter();
|
||||
|
||||
return printer.printNode(
|
||||
ts.EmitHint.Unspecified,
|
||||
result.transformed[0],
|
||||
tsNode
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Crawls pages directory and returns all Astro pages
|
||||
* except for locale folders and excluded pages / directories (starting with underscore).
|
||||
* (https://docs.astro.build/en/core-concepts/routing/#excluding-pages)
|
||||
*
|
||||
* @param pagesDirectoryPath
|
||||
* @param childDirToCrawl will make the function crawl inside the given
|
||||
* `childDirToCrawl` (doesn't take paths, only dirname).
|
||||
*/
|
||||
export const getAstroPagesFullPaths = (
|
||||
pagesDirectoryPath: string,
|
||||
childDirToCrawl: AstroI18nextConfig["defaultLocale"] | undefined = undefined,
|
||||
locales: AstroI18nextConfig["locales"] = []
|
||||
): PathsOutput => {
|
||||
// eslint-disable-next-line new-cap
|
||||
const api = new fdir()
|
||||
.filter(
|
||||
(filepath) => !isFileHidden(filepath) && filepath.endsWith(".astro")
|
||||
)
|
||||
.exclude((dirName) => locales.includes(dirName))
|
||||
.withFullPaths();
|
||||
|
||||
return childDirToCrawl
|
||||
? (api
|
||||
.crawl(`${pagesDirectoryPath}${path.sep}${childDirToCrawl}`)
|
||||
.sync() as PathsOutput)
|
||||
: (api.crawl(pagesDirectoryPath).sync() as PathsOutput);
|
||||
};
|
||||
|
||||
export const createFiles = (filesToGenerate: FileToGenerate[]): void => {
|
||||
filesToGenerate.forEach((fileToGenerate) => {
|
||||
fsExtra.ensureDirSync(path.dirname(fileToGenerate.path));
|
||||
|
||||
fs.writeFileSync(fileToGenerate.path, fileToGenerate.source);
|
||||
});
|
||||
};
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* Resolves the right translated path based on
|
||||
* a given `astroFilePath` and a locale,
|
||||
* with the `routeTranslations` mapping.
|
||||
*/
|
||||
export const resolveTranslatedAstroPath = (
|
||||
astroFilePath: string,
|
||||
locale: string | null = null,
|
||||
basePath: string = "",
|
||||
flatRoutes: AstroI18nextConfig["flatRoutes"] = {}
|
||||
) => {
|
||||
astroFilePath = astroFilePath.replace(/^\/+|\/+$/g, "");
|
||||
|
||||
// remove trailing slash of basePath if any
|
||||
basePath = basePath.replace(/\/+$/g, "");
|
||||
|
||||
if (locale === null) {
|
||||
return `${basePath}/${astroFilePath}`;
|
||||
}
|
||||
|
||||
astroFilePath = astroFilePath.replace(/.astro$/, "");
|
||||
|
||||
const filePath = `/${locale}/${astroFilePath}`;
|
||||
|
||||
// is route translated?
|
||||
if (Object.prototype.hasOwnProperty.call(flatRoutes, filePath)) {
|
||||
return `${basePath}${flatRoutes[filePath]}.astro`;
|
||||
}
|
||||
|
||||
return `${basePath}/${locale}/${astroFilePath}.astro`;
|
||||
};
|
17
node_modules/astro-i18next/src/components/HeadHrefLangs.astro
generated
vendored
Normal file
17
node_modules/astro-i18next/src/components/HeadHrefLangs.astro
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
import i18next from "i18next";
|
||||
import { localizeUrl } from "../..";
|
||||
|
||||
const supportedLanguages = i18next.languages;
|
||||
const currentUrl = Astro.url.href;
|
||||
---
|
||||
|
||||
{
|
||||
supportedLanguages.map((supportedLanguage) => (
|
||||
<link
|
||||
rel="alternate"
|
||||
hreflang={supportedLanguage}
|
||||
href={localizeUrl(currentUrl, supportedLanguage)}
|
||||
/>
|
||||
))
|
||||
}
|
49
node_modules/astro-i18next/src/components/LanguageSelector.astro
generated
vendored
Normal file
49
node_modules/astro-i18next/src/components/LanguageSelector.astro
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
import i18next from "i18next";
|
||||
import { localizePath } from "../..";
|
||||
import localeEmoji from "locale-emoji";
|
||||
import ISO6991 from "iso-639-1";
|
||||
|
||||
interface languageMapping {
|
||||
[localeCode: string]: string;
|
||||
}
|
||||
|
||||
export interface Props extends astroHTML.JSX.SelectHTMLAttributes {
|
||||
showFlag?: boolean;
|
||||
languageMapping?: languageMapping;
|
||||
}
|
||||
|
||||
const supportedLanguages = i18next.languages;
|
||||
const currentLanguage = i18next.language;
|
||||
|
||||
const { pathname } = Astro.url;
|
||||
|
||||
const { showFlag = false, languageMapping, ...attributes } = Astro.props;
|
||||
---
|
||||
|
||||
<select onchange="location = this.value;" {...attributes}>
|
||||
{
|
||||
supportedLanguages.map((supportedLanguage: string) => {
|
||||
let value = localizePath(pathname, supportedLanguage);
|
||||
const flag = showFlag ? localeEmoji(supportedLanguage) + " " : "";
|
||||
|
||||
let nativeName = "";
|
||||
if (
|
||||
languageMapping &&
|
||||
languageMapping.hasOwnProperty(supportedLanguage)
|
||||
) {
|
||||
nativeName = languageMapping[supportedLanguage];
|
||||
} else {
|
||||
nativeName = ISO6991.getNativeName(supportedLanguage);
|
||||
}
|
||||
|
||||
const label = flag + nativeName;
|
||||
|
||||
return (
|
||||
<option value={value} selected={supportedLanguage === currentLanguage}>
|
||||
{label}
|
||||
</option>
|
||||
);
|
||||
})
|
||||
}
|
||||
</select>
|
21
node_modules/astro-i18next/src/components/Trans.astro
generated
vendored
Normal file
21
node_modules/astro-i18next/src/components/Trans.astro
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
import { interpolate, createReferenceStringFromHTML } from "..";
|
||||
|
||||
export interface Props {
|
||||
i18nKey?: string;
|
||||
ns?: string;
|
||||
}
|
||||
|
||||
const { i18nKey, ns } = Astro.props;
|
||||
|
||||
const referenceString = await Astro.slots.render("default");
|
||||
|
||||
let key: string;
|
||||
if (typeof i18nKey === "string") {
|
||||
key = i18nKey;
|
||||
} else {
|
||||
key = createReferenceStringFromHTML(referenceString);
|
||||
}
|
||||
---
|
||||
|
||||
<Fragment set:html={interpolate(key, referenceString, ns)} />
|
3
node_modules/astro-i18next/src/components/index.d.ts
generated
vendored
Normal file
3
node_modules/astro-i18next/src/components/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as Trans } from "./Trans.astro";
|
||||
export { default as LanguageSelector } from "./LanguageSelector.astro";
|
||||
export { default as HeadHrefLangs } from "./HeadHrefLangs.astro";
|
3
node_modules/astro-i18next/src/components/index.ts
generated
vendored
Normal file
3
node_modules/astro-i18next/src/components/index.ts
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as Trans } from "./Trans.astro";
|
||||
export { default as LanguageSelector } from "./LanguageSelector.astro";
|
||||
export { default as HeadHrefLangs } from "./HeadHrefLangs.astro";
|
89
node_modules/astro-i18next/src/config.ts
generated
vendored
Normal file
89
node_modules/astro-i18next/src/config.ts
generated
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
import { AstroI18nextConfig, AstroI18nextGlobal, Routes } from "./types";
|
||||
|
||||
export const AstroI18next: AstroI18nextGlobal = {
|
||||
config: {
|
||||
defaultLocale: "cimode",
|
||||
locales: [],
|
||||
namespaces: "translation",
|
||||
defaultNamespace: "translation",
|
||||
load: ["server"],
|
||||
routes: {},
|
||||
flatRoutes: {},
|
||||
showDefaultLocale: false,
|
||||
trailingSlash: "ignore",
|
||||
resourcesBasePath: "/locales",
|
||||
},
|
||||
};
|
||||
|
||||
/* c8 ignore start */
|
||||
export const setAstroI18nextConfig = (config: AstroI18nextConfig) => {
|
||||
let flatRoutes = {};
|
||||
for (const key in config) {
|
||||
if (key === "routes") {
|
||||
flatRoutes = flattenRoutes(config[key]);
|
||||
}
|
||||
|
||||
AstroI18next.config[key] = config[key];
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
AstroI18next.config.flatRoutes = flatRoutes;
|
||||
};
|
||||
|
||||
export const astroI18nextConfigBuilder = (
|
||||
config: AstroI18nextConfig
|
||||
): AstroI18nextConfig => {
|
||||
return { ...AstroI18next.config, ...config };
|
||||
};
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* This will create a mapping of translated routes to search them easily.
|
||||
*
|
||||
* TODO: render all routes mappings in here (even those not translated),
|
||||
* this will help simplify utility functions logic
|
||||
*/
|
||||
export const flattenRoutes = (
|
||||
routes: AstroI18nextConfig["routes"],
|
||||
previous: string[] = [],
|
||||
translatedPrevious: string[] = [],
|
||||
prevResult: AstroI18nextConfig["flatRoutes"] = null
|
||||
): AstroI18nextConfig["flatRoutes"] => {
|
||||
let result = prevResult || {};
|
||||
|
||||
for (const i in routes) {
|
||||
if (typeof routes[i] === "object" && routes[i] !== null) {
|
||||
// Recursion on deeper objects
|
||||
flattenRoutes(
|
||||
routes[i] as Routes,
|
||||
[...previous, i],
|
||||
[
|
||||
...translatedPrevious,
|
||||
Object.prototype.hasOwnProperty.call(routes[i], "index")
|
||||
? routes[i]["index"]
|
||||
: i,
|
||||
],
|
||||
result
|
||||
);
|
||||
} else {
|
||||
let key = "/" + previous.join("/");
|
||||
let value = "/" + translatedPrevious.join("/");
|
||||
|
||||
if (i === "index") {
|
||||
result[key] = value;
|
||||
|
||||
key += "/" + i;
|
||||
value += "/" + i;
|
||||
|
||||
result[key] = value;
|
||||
} else {
|
||||
key += "/" + i;
|
||||
value += "/" + routes[i];
|
||||
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
185
node_modules/astro-i18next/src/index.ts
generated
vendored
Normal file
185
node_modules/astro-i18next/src/index.ts
generated
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
import { AstroIntegration } from "astro";
|
||||
import { InitOptions } from "i18next";
|
||||
import { astroI18nextConfigBuilder, setAstroI18nextConfig } from "./config";
|
||||
import { AstroI18nextConfig, AstroI18nextOptions, Plugins } from "./types";
|
||||
import {
|
||||
moveDefaultLocaleToFirstIndex,
|
||||
deeplyStringifyObject,
|
||||
getUserConfig,
|
||||
} from "./utils";
|
||||
import { resolve } from "pathe";
|
||||
|
||||
export default (options?: AstroI18nextOptions): AstroIntegration => {
|
||||
const customConfigPath = options?.configPath;
|
||||
|
||||
return {
|
||||
name: "astro-i18next",
|
||||
hooks: {
|
||||
"astro:config:setup": async ({ config, injectScript }) => {
|
||||
/**
|
||||
* 0. Get user config
|
||||
*/
|
||||
const userConfig = await getUserConfig(config.root, customConfigPath);
|
||||
|
||||
if (customConfigPath && !userConfig?.value) {
|
||||
throw new Error(
|
||||
`[astro-i18next]: Could not find a config file at ${JSON.stringify(
|
||||
customConfigPath
|
||||
)}. Does the file exist?`
|
||||
);
|
||||
}
|
||||
|
||||
const astroI18nextConfig: AstroI18nextConfig =
|
||||
astroI18nextConfigBuilder(userConfig?.value as AstroI18nextConfig);
|
||||
|
||||
/**
|
||||
* 1. Validate and prepare config
|
||||
*/
|
||||
if (
|
||||
!astroI18nextConfig.defaultLocale ||
|
||||
astroI18nextConfig.defaultLocale === ""
|
||||
) {
|
||||
throw new Error(
|
||||
"[astro-i18next]: you must set a `defaultLocale` in your astro-i18next config!"
|
||||
);
|
||||
}
|
||||
|
||||
if (!astroI18nextConfig.locales) {
|
||||
astroI18nextConfig.locales = [astroI18nextConfig.defaultLocale];
|
||||
}
|
||||
|
||||
if (
|
||||
!astroI18nextConfig.locales.includes(astroI18nextConfig.defaultLocale)
|
||||
) {
|
||||
astroI18nextConfig.locales.unshift(astroI18nextConfig.defaultLocale);
|
||||
}
|
||||
|
||||
// make sure to have default locale set as first element in locales (for supportedLngs)
|
||||
if (
|
||||
astroI18nextConfig.locales[0] !== astroI18nextConfig.defaultLocale
|
||||
) {
|
||||
moveDefaultLocaleToFirstIndex(
|
||||
astroI18nextConfig.locales as string[],
|
||||
astroI18nextConfig.defaultLocale
|
||||
);
|
||||
}
|
||||
|
||||
// add trailingSlash config from astro if not set
|
||||
astroI18nextConfig.trailingSlash = config.trailingSlash;
|
||||
|
||||
if (astroI18nextConfig.load.includes("server")) {
|
||||
// Build server side i18next config
|
||||
// set i18next supported and fallback languages (same as locales)
|
||||
const serverConfig: InitOptions = {
|
||||
supportedLngs: astroI18nextConfig.locales as string[],
|
||||
fallbackLng: astroI18nextConfig.locales as string[],
|
||||
ns: astroI18nextConfig.namespaces,
|
||||
defaultNS: astroI18nextConfig.defaultNamespace,
|
||||
initImmediate: false,
|
||||
backend: {
|
||||
loadPath: resolve(
|
||||
`${config.publicDir.pathname}/${astroI18nextConfig.resourcesBasePath}/{{lng}}/{{ns}}.json`
|
||||
),
|
||||
},
|
||||
...astroI18nextConfig.i18nextServer,
|
||||
};
|
||||
|
||||
const defaultI18nextServerPlugins: Plugins = {
|
||||
fsBackend: "i18next-fs-backend",
|
||||
};
|
||||
|
||||
const i18nextServerPlugins = {
|
||||
...defaultI18nextServerPlugins,
|
||||
...astroI18nextConfig.i18nextServerPlugins,
|
||||
};
|
||||
|
||||
let { imports: serverImports, i18nextInit: i18nextInitServer } =
|
||||
i18nextScriptBuilder(serverConfig, i18nextServerPlugins);
|
||||
|
||||
// initializing runtime astro-i18next config
|
||||
serverImports += `import {initAstroI18next} from "astro-i18next";`;
|
||||
const astroI18nextInit = `initAstroI18next(${deeplyStringifyObject(
|
||||
astroI18nextConfig
|
||||
)});`;
|
||||
|
||||
// server side i18next instance
|
||||
injectScript(
|
||||
"page-ssr",
|
||||
serverImports + i18nextInitServer + astroI18nextInit
|
||||
);
|
||||
}
|
||||
|
||||
if (astroI18nextConfig.load?.includes("client")) {
|
||||
const clientConfig: InitOptions = {
|
||||
supportedLngs: astroI18nextConfig.locales as string[],
|
||||
fallbackLng: astroI18nextConfig.locales as string[],
|
||||
ns: astroI18nextConfig.namespaces,
|
||||
defaultNS: astroI18nextConfig.defaultNamespace,
|
||||
detection: {
|
||||
order: ["htmlTag"],
|
||||
caches: [],
|
||||
},
|
||||
backend: {
|
||||
loadPath: `${astroI18nextConfig.resourcesBasePath}/{{lng}}/{{ns}}.json`,
|
||||
},
|
||||
...astroI18nextConfig.i18nextClient,
|
||||
};
|
||||
|
||||
const defaultI18nextClientPlugins: Plugins = {
|
||||
httpBackend: "i18next-http-backend",
|
||||
LanguageDetector: "i18next-browser-languagedetector",
|
||||
};
|
||||
|
||||
const i18nextClientPlugins = {
|
||||
...defaultI18nextClientPlugins,
|
||||
...astroI18nextConfig.i18nextClientPlugins,
|
||||
};
|
||||
|
||||
let { imports: clientImports, i18nextInit: i18nextInitClient } =
|
||||
i18nextScriptBuilder(clientConfig, i18nextClientPlugins);
|
||||
|
||||
// client side i18next instance
|
||||
injectScript("before-hydration", clientImports + i18nextInitClient);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const i18nextScriptBuilder = (config: InitOptions, plugins: Plugins) => {
|
||||
let imports = `import i18next from "i18next";`;
|
||||
let i18nextInit = "i18next";
|
||||
|
||||
if (Object.keys(plugins).length > 0) {
|
||||
for (const key of Object.keys(plugins)) {
|
||||
// discard plugin if it does not have import name
|
||||
if (plugins[key] === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
imports += `import ${key} from "${plugins[key]}";`;
|
||||
i18nextInit += `.use(${key.replace(/[{}]/g, "")})`;
|
||||
}
|
||||
}
|
||||
|
||||
i18nextInit += `.init(${deeplyStringifyObject(config)});`;
|
||||
|
||||
return { imports, i18nextInit };
|
||||
};
|
||||
|
||||
export function initAstroI18next(config: AstroI18nextConfig) {
|
||||
// init runtime config
|
||||
setAstroI18nextConfig(config);
|
||||
}
|
||||
|
||||
export { AstroI18next } from "./config";
|
||||
|
||||
export {
|
||||
createReferenceStringFromHTML,
|
||||
detectLocaleFromPath,
|
||||
interpolate,
|
||||
localizePath,
|
||||
localizeUrl,
|
||||
} from "./utils";
|
||||
|
||||
export { AstroI18nextConfig, AstroI18nextOptions } from "./types";
|
134
node_modules/astro-i18next/src/types.ts
generated
vendored
Normal file
134
node_modules/astro-i18next/src/types.ts
generated
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
import { InitOptions } from "i18next";
|
||||
|
||||
export interface AstroI18nextGlobal {
|
||||
config: AstroI18nextConfig;
|
||||
}
|
||||
|
||||
export interface AstroI18nextOptions {
|
||||
/**
|
||||
* Path to your astro-i18next config file
|
||||
*
|
||||
* @default 'astro-i18next.config.js'
|
||||
*/
|
||||
configPath?: string;
|
||||
}
|
||||
|
||||
export interface Routes {
|
||||
[segment: string]: string | Record<string, string | Routes>;
|
||||
}
|
||||
|
||||
export interface Plugins {
|
||||
[importName: string]: string | null;
|
||||
}
|
||||
|
||||
export interface AstroI18nextConfig {
|
||||
/**
|
||||
* The default locale for your website.
|
||||
*
|
||||
* @default "cimode"
|
||||
*/
|
||||
defaultLocale: string;
|
||||
|
||||
/**
|
||||
* The locales that are supported by your website.
|
||||
*
|
||||
* @default []
|
||||
*/
|
||||
locales: string[];
|
||||
|
||||
/**
|
||||
* String or array of namespaces to load
|
||||
*
|
||||
* @default "translation"
|
||||
*/
|
||||
namespaces?: string | string[];
|
||||
|
||||
/**
|
||||
* Default namespace used if not passed to the translation function
|
||||
*
|
||||
* @default "translation"
|
||||
*/
|
||||
defaultNamespace?: string;
|
||||
|
||||
/**
|
||||
* Load i18next on server side only, client side only or both.
|
||||
*
|
||||
* @default ["server"]
|
||||
*/
|
||||
load?: ("server" | "client")[];
|
||||
|
||||
/**
|
||||
* Set base path for i18next resources.
|
||||
*
|
||||
* @default "/locales"
|
||||
*/
|
||||
resourcesBasePath?: string;
|
||||
|
||||
/**
|
||||
* i18next server side config. See https://www.i18next.com/overview/configuration-options
|
||||
*/
|
||||
i18nextServer?: InitOptions;
|
||||
|
||||
/**
|
||||
* i18next client side config. See https://www.i18next.com/overview/configuration-options
|
||||
*/
|
||||
i18nextClient?: InitOptions;
|
||||
|
||||
/**
|
||||
* The translations for your routes.
|
||||
*
|
||||
* @default {}
|
||||
*/
|
||||
routes?: Routes;
|
||||
|
||||
/**
|
||||
* Generated mappings based on the routes
|
||||
*
|
||||
* @default {}
|
||||
*/
|
||||
readonly flatRoutes?: Record<string, string>;
|
||||
|
||||
/**
|
||||
* The display behaviour for the URL locale.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
showDefaultLocale?: boolean;
|
||||
|
||||
/**
|
||||
* i18next server side plugins. See https://www.i18next.com/overview/plugins-and-utils
|
||||
*
|
||||
* Include the plugins with the key being the import name and the value being the plugin name.
|
||||
*
|
||||
* Eg.:
|
||||
* ```
|
||||
* {
|
||||
* "Backend": "i18next-fs-backend",
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
i18nextServerPlugins?: Plugins;
|
||||
|
||||
/**
|
||||
* i18next client side plugins. See https://www.i18next.com/overview/plugins-and-utils
|
||||
*
|
||||
* Include the plugins with the key being the import name and the value being the plugin name.
|
||||
*
|
||||
* Eg.:
|
||||
* ```
|
||||
* {
|
||||
* "{initReactI18next}": "react-i18next",
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
i18nextClientPlugins?: Plugins;
|
||||
|
||||
/**
|
||||
* Set the route matching behavior of the dev server. Choose from the following options:
|
||||
*
|
||||
* 'always' - Only match URLs that include a trailing slash (ex: "/foo/")
|
||||
* 'never' - Never match URLs that include a trailing slash (ex: "/foo")
|
||||
* 'ignore' - Match URLs regardless of whether a trailing "/" exists
|
||||
*/
|
||||
trailingSlash?: "always" | "never" | "ignore";
|
||||
}
|
387
node_modules/astro-i18next/src/utils.ts
generated
vendored
Normal file
387
node_modules/astro-i18next/src/utils.ts
generated
vendored
Normal file
@@ -0,0 +1,387 @@
|
||||
import i18next, { t } from "i18next";
|
||||
import { fileURLToPath } from "url";
|
||||
import load from "@proload/core";
|
||||
import { AstroI18nextConfig } from "./types";
|
||||
import typescript from "@proload/plugin-tsm";
|
||||
import { AstroI18next } from "./config";
|
||||
|
||||
/**
|
||||
* Adapted from astro's tailwind integration:
|
||||
* https://github.com/withastro/astro/tree/main/packages/integrations/tailwind
|
||||
*/
|
||||
/* c8 ignore start */
|
||||
export const getUserConfig = async (
|
||||
root: URL,
|
||||
configPath?: string
|
||||
): Promise<load.Config<AstroI18nextConfig>> => {
|
||||
const resolvedRoot = fileURLToPath(root);
|
||||
let userConfigPath: string | undefined;
|
||||
|
||||
if (configPath) {
|
||||
const configPathWithLeadingSlash = /^\.*\//.test(configPath)
|
||||
? configPath
|
||||
: `./${configPath}`;
|
||||
|
||||
userConfigPath = fileURLToPath(new URL(configPathWithLeadingSlash, root));
|
||||
}
|
||||
|
||||
load.use([typescript]);
|
||||
return (await load("astro-i18next", {
|
||||
mustExist: false,
|
||||
cwd: resolvedRoot,
|
||||
filePath: userConfigPath,
|
||||
})) as load.Config<AstroI18nextConfig>;
|
||||
};
|
||||
/* c8 ignore stop */
|
||||
|
||||
/**
|
||||
* Moves the default locale in the first index
|
||||
*/
|
||||
export const moveDefaultLocaleToFirstIndex = (
|
||||
locales: string[],
|
||||
baseLocale: string
|
||||
): void => {
|
||||
const baseLocaleIndex = locales.indexOf(baseLocale);
|
||||
locales.splice(baseLocaleIndex, 1);
|
||||
locales.unshift(baseLocale);
|
||||
};
|
||||
|
||||
/**
|
||||
* Interpolates a localized string (loaded with the i18nKey) to a given reference string.
|
||||
*/
|
||||
export const interpolate = (
|
||||
i18nKey: string,
|
||||
referenceString: string,
|
||||
namespace: string | null = null
|
||||
): string => {
|
||||
const localizedString = t(i18nKey, { ns: namespace });
|
||||
|
||||
if (localizedString === i18nKey) {
|
||||
console.warn(`WARNING(astro-i18next): missing translation key ${i18nKey}.`);
|
||||
return referenceString;
|
||||
}
|
||||
|
||||
const tagsRegex = /<([\w\d]+)([^>]*)>/gi;
|
||||
|
||||
const referenceStringMatches = referenceString.match(tagsRegex);
|
||||
|
||||
if (!referenceStringMatches) {
|
||||
console.warn(
|
||||
"WARNING(astro-i18next): default slot does not include any HTML tag to interpolate! You should use the `t` function directly."
|
||||
);
|
||||
return localizedString;
|
||||
}
|
||||
|
||||
const referenceTags = [];
|
||||
referenceStringMatches.forEach((tagNode) => {
|
||||
const [, name, attributes] = tagsRegex.exec(tagNode);
|
||||
referenceTags.push({ name, attributes });
|
||||
|
||||
// reset regex state
|
||||
tagsRegex.exec("");
|
||||
});
|
||||
|
||||
let interpolatedString = localizedString;
|
||||
for (let index = 0; index < referenceTags.length; index++) {
|
||||
const referencedTag = referenceTags[index];
|
||||
// Replace opening tags
|
||||
interpolatedString = interpolatedString.replaceAll(
|
||||
`<${index}>`,
|
||||
`<${referencedTag.name}${referencedTag.attributes}>`
|
||||
);
|
||||
// Replace closing tags
|
||||
interpolatedString = interpolatedString.replaceAll(
|
||||
`</${index}>`,
|
||||
`</${referencedTag.name}>`
|
||||
);
|
||||
}
|
||||
|
||||
return interpolatedString;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a reference string from an HTML string. The reverse of interpolate(), for use
|
||||
* with <Trans> when not explicitly setting a key
|
||||
*/
|
||||
export const createReferenceStringFromHTML = (html: string) => {
|
||||
// Allow these tags to carry through to the output
|
||||
const allowedTags = ["strong", "br", "em", "i", "b"];
|
||||
|
||||
let forbiddenStrings: { key: string; str: string }[] = [];
|
||||
if (i18next.options) {
|
||||
forbiddenStrings = [
|
||||
"keySeparator",
|
||||
"nsSeparator",
|
||||
"pluralSeparator",
|
||||
"contextSeparator",
|
||||
]
|
||||
.map((key) => {
|
||||
return {
|
||||
key,
|
||||
str: i18next.options[key],
|
||||
};
|
||||
})
|
||||
.filter(function <T>(val: T | undefined): val is T {
|
||||
return typeof val !== "undefined";
|
||||
});
|
||||
}
|
||||
|
||||
const tagsRegex = /<([\w\d]+)([^>]*)>/gi;
|
||||
|
||||
const referenceStringMatches = html.match(tagsRegex);
|
||||
|
||||
if (!referenceStringMatches) {
|
||||
console.warn(
|
||||
"WARNING(astro-i18next): default slot does not include any HTML tag to interpolate! You should use the `t` function directly."
|
||||
);
|
||||
return html;
|
||||
}
|
||||
|
||||
const referenceTags = [];
|
||||
referenceStringMatches.forEach((tagNode) => {
|
||||
const [, name, attributes] = tagsRegex.exec(tagNode);
|
||||
referenceTags.push({ name, attributes });
|
||||
|
||||
// reset regex state
|
||||
tagsRegex.exec("");
|
||||
});
|
||||
|
||||
let sanitizedString = html.replace(/\s+/g, " ").trim();
|
||||
for (let index = 0; index < referenceTags.length; index++) {
|
||||
const referencedTag = referenceTags[index];
|
||||
if (
|
||||
allowedTags.includes(referencedTag.name) &&
|
||||
referencedTag.attributes.trim().length === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
sanitizedString = sanitizedString.replaceAll(
|
||||
new RegExp(`<${referencedTag.name}[^>]*?\\s*\\/>`, "gi"),
|
||||
`<${index}/>`
|
||||
);
|
||||
sanitizedString = sanitizedString.replaceAll(
|
||||
`<${referencedTag.name}${referencedTag.attributes}>`,
|
||||
`<${index}>`
|
||||
);
|
||||
sanitizedString = sanitizedString.replaceAll(
|
||||
`</${referencedTag.name}>`,
|
||||
`</${index}>`
|
||||
);
|
||||
}
|
||||
|
||||
for (let index = 0; index < forbiddenStrings.length; index++) {
|
||||
const { key, str } = forbiddenStrings[index];
|
||||
if (sanitizedString.includes(str)) {
|
||||
console.warn(
|
||||
`WARNING(astro-i18next): "${str}" was found in a <Trans> translation key, but it is also used as ${key}. Either explicitly set an i18nKey or change the value of ${key}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
return sanitizedString;
|
||||
};
|
||||
|
||||
export const handleTrailingSlash = (
|
||||
path: string,
|
||||
trailingSlash: AstroI18nextConfig["trailingSlash"]
|
||||
) => {
|
||||
if (path === "/") {
|
||||
return path;
|
||||
}
|
||||
|
||||
switch (trailingSlash) {
|
||||
case "always":
|
||||
return path.endsWith("/") ? path : path + "/";
|
||||
case "never":
|
||||
return path.replace(/\/$/, "");
|
||||
default:
|
||||
return path;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Injects the given locale to a path
|
||||
*/
|
||||
export const localizePath = (
|
||||
path: string = "/",
|
||||
locale: string | null = null,
|
||||
base: string = import.meta.env.BASE_URL
|
||||
): string => {
|
||||
if (!locale) {
|
||||
locale = i18next.language;
|
||||
}
|
||||
|
||||
let pathSegments = path.split("/").filter((segment) => segment !== "");
|
||||
const baseSegments = base.split("/").filter((segment) => segment !== "");
|
||||
|
||||
if (
|
||||
JSON.stringify(pathSegments).startsWith(
|
||||
JSON.stringify(baseSegments).replace(/]+$/, "")
|
||||
)
|
||||
) {
|
||||
// remove base from path
|
||||
pathSegments.splice(0, baseSegments.length);
|
||||
}
|
||||
|
||||
path = pathSegments.length === 0 ? "" : pathSegments.join("/");
|
||||
base = baseSegments.length === 0 ? "/" : "/" + baseSegments.join("/") + "/";
|
||||
|
||||
const {
|
||||
flatRoutes,
|
||||
showDefaultLocale,
|
||||
defaultLocale,
|
||||
locales,
|
||||
trailingSlash,
|
||||
} = AstroI18next.config;
|
||||
|
||||
if (!locales.includes(locale)) {
|
||||
console.warn(
|
||||
`WARNING(astro-i18next): "${locale}" locale is not supported, add it to the locales in your astro config.`
|
||||
);
|
||||
return handleTrailingSlash(`${base}${path}`, trailingSlash);
|
||||
}
|
||||
|
||||
if (pathSegments.length === 0) {
|
||||
if (showDefaultLocale) {
|
||||
return handleTrailingSlash(`${base}${locale}`, trailingSlash);
|
||||
}
|
||||
|
||||
return handleTrailingSlash(
|
||||
locale === defaultLocale ? base : `${base}${locale}`,
|
||||
trailingSlash
|
||||
);
|
||||
}
|
||||
|
||||
// check if the path is not already present in flatRoutes
|
||||
if (locale === defaultLocale) {
|
||||
const translatedPathKey = Object.keys(flatRoutes).find(
|
||||
(key) => flatRoutes[key] === "/" + path
|
||||
);
|
||||
if (typeof translatedPathKey !== "undefined") {
|
||||
pathSegments = translatedPathKey
|
||||
.split("/")
|
||||
.filter((segment) => segment !== "");
|
||||
}
|
||||
}
|
||||
|
||||
// remove locale from pathSegments (if there is any)
|
||||
for (const locale of locales) {
|
||||
if (pathSegments[0] === locale) {
|
||||
pathSegments.shift();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// prepend the given locale if it's not the base one (unless showDefaultLocale)
|
||||
if (showDefaultLocale || locale !== defaultLocale) {
|
||||
pathSegments = [locale, ...pathSegments];
|
||||
}
|
||||
|
||||
const localizedPath = base + pathSegments.join("/");
|
||||
|
||||
// is path translated?
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
flatRoutes,
|
||||
localizedPath.replace(/\/$/, "")
|
||||
)
|
||||
) {
|
||||
return handleTrailingSlash(
|
||||
flatRoutes[localizedPath.replace(/\/$/, "")],
|
||||
trailingSlash
|
||||
);
|
||||
}
|
||||
|
||||
return handleTrailingSlash(localizedPath, trailingSlash);
|
||||
};
|
||||
|
||||
/**
|
||||
* Injects the given locale to a url
|
||||
*/
|
||||
export const localizeUrl = (
|
||||
url: string,
|
||||
locale: string | null = null,
|
||||
base: string = import.meta.env.BASE_URL
|
||||
): string => {
|
||||
const [protocol, , host, ...path] = url.split("/");
|
||||
const baseUrl = protocol + "//" + host;
|
||||
|
||||
return baseUrl + localizePath(path.join("/"), locale, base);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the locale detected from a given path
|
||||
*/
|
||||
export const detectLocaleFromPath = (path: string) => {
|
||||
// remove all leading slashes
|
||||
path = path.replace(/^\/+/g, "");
|
||||
|
||||
const { defaultLocale, locales } = AstroI18next.config;
|
||||
|
||||
const pathSegments = path.split("/");
|
||||
|
||||
if (
|
||||
JSON.stringify(pathSegments) === JSON.stringify([""]) ||
|
||||
JSON.stringify(pathSegments) === JSON.stringify(["", ""])
|
||||
) {
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
// make a copy of supported locales
|
||||
let otherLocales = [...locales];
|
||||
otherLocales = otherLocales.filter((locale) => locale !== defaultLocale); // remove base locale (first index)
|
||||
|
||||
// loop over all locales except the base one
|
||||
for (const otherLocale of otherLocales) {
|
||||
if (pathSegments[0] === otherLocale) {
|
||||
// if the path starts with one of the other locales, then detected!
|
||||
return otherLocale;
|
||||
}
|
||||
}
|
||||
|
||||
// return default locale by default
|
||||
return defaultLocale;
|
||||
};
|
||||
|
||||
export const deeplyStringifyObject = (obj: object | Array<any>): string => {
|
||||
const isArray = Array.isArray(obj);
|
||||
let str = isArray ? "[" : "{";
|
||||
for (const key in obj) {
|
||||
if (obj[key] === null || obj[key] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = null;
|
||||
|
||||
// see typeof result: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof#description
|
||||
switch (typeof obj[key]) {
|
||||
case "string": {
|
||||
value = `"${obj[key]}"`;
|
||||
break;
|
||||
}
|
||||
case "number":
|
||||
case "boolean": {
|
||||
value = obj[key];
|
||||
break;
|
||||
}
|
||||
case "object": {
|
||||
value = deeplyStringifyObject(obj[key]);
|
||||
break;
|
||||
}
|
||||
case "function": {
|
||||
value = obj[key].toString().replace(/\s+/g, " ");
|
||||
break;
|
||||
}
|
||||
case "symbol": {
|
||||
value = `Symbol("${obj[key].description}")`;
|
||||
break;
|
||||
}
|
||||
/* c8 ignore start */
|
||||
default:
|
||||
break;
|
||||
/* c8 ignore stop */
|
||||
}
|
||||
|
||||
str += isArray ? `${value},` : `"${key}": ${value},`;
|
||||
}
|
||||
return `${str}${isArray ? "]" : "}"}`;
|
||||
};
|
Reference in New Issue
Block a user