astro native i18n + i18n utils

This commit is contained in:
nicolas arduin 2024-12-28 11:52:32 +01:00
parent 99ef7634e3
commit b7ce5b7f20
Signed by: nicolas
SSH Key Fingerprint: SHA256:ELi8eDeNLl5PTn64G+o2Kx5+XVDfHF5um2tZigfwWkM
16 changed files with 266 additions and 163 deletions

View File

@ -1,52 +0,0 @@
type DefaultLangCode = "fr"
type SupportedLangCode = "en"
type LangCode = DefaultLangCode | SupportedLangCode
type RouteUri = | "/articles/[slug]" | "/articles" | "/agments/[slug]" | "/agments" | "/references" | "/veille" | "/" | "/plan-du-site"
type RouteParams = {"/articles/[slug]": { "slug": string; }; "/articles": undefined; "/agments/[slug]": { "slug": string; }; "/agments": undefined; "/references": undefined; "/veille": undefined; "/": undefined; "/plan-du-site": undefined; }
type TranslationPath = "accueil" | "tagline" | "copyright" | "contact.title" | "contact.email" | "contact.tel" | "contenuVide" | "header.skipLink" | "header.mainNav" | "header.homeLink" | "sitemap" | "prevNext.contenus" | "prevNext.precedent" | "prevNext.suivant" | "article.titre" | "article.tagline" | "article.published" | "meta.publication" | "meta.modification" | "meta.credit" | "fragments.titre" | "fragments.tagline" | "references.titre" | "references.slug" | "references.cta" | "references.tagline" | "veille.titre" | "veille.tagline" | "erreur.introuvable" | "erreur.autre" | "erreur.lienRetour" | "seo.meta.description" | "seo.article.title" | "seo.article.description" | "seo.code.title" | "seo.code.description" | "seo.references.title" | "seo.references.description" | "index.articles.pageName" | "index.articles.subtitle" | "index.fragments.pageName" | "index.fragments.subtitle" | "index.references.pageName" | "index.references.subtitle" | "index.veille.pageName" | "index.veille.subtitle" | "index.title" | "index.subtitle" | "index.quoi" | "index.comment" | "index.opensource" | "index.writing" | "index.latestProjects" | "index.latestArticles" | "index.allProjects" | "index.allArticles" | "index.latestSnippets" | "index.allSnippets" | "index.toc" | "contact.contenuVide"
type TranslationOptions = { "accueil": {} | undefined; "tagline": {} | undefined; "copyright": {} | undefined; "contact.title": {} | undefined; "contact.email": {} | undefined; "contact.tel": {} | undefined; "contenuVide": {} | undefined; "header.skipLink": {} | undefined; "header.mainNav": {} | undefined; "header.homeLink": {} | undefined; "sitemap": {} | undefined; "prevNext.contenus": {} | undefined; "prevNext.precedent": {} | undefined; "prevNext.suivant": {} | undefined; "article.titre": {} | undefined; "article.tagline": {} | undefined; "article.published": { datetime: unknown; options: unknown; }; "meta.publication": {} | undefined; "meta.modification": {} | undefined; "meta.credit": {} | undefined; "fragments.titre": {} | undefined; "fragments.tagline": {} | undefined; "references.titre": {} | undefined; "references.slug": {} | undefined; "references.cta": {} | undefined; "references.tagline": {} | undefined; "veille.titre": {} | undefined; "veille.tagline": {} | undefined; "erreur.introuvable": {} | undefined; "erreur.autre": {} | undefined; "erreur.lienRetour": {} | undefined; "seo.meta.description": {} | undefined; "seo.article.title": {} | undefined; "seo.article.description": {} | undefined; "seo.code.title": {} | undefined; "seo.code.description": {} | undefined; "seo.references.title": {} | undefined; "seo.references.description": {} | undefined; "index.articles.pageName": {} | undefined; "index.articles.subtitle": {} | undefined; "index.fragments.pageName": {} | undefined; "index.fragments.subtitle": {} | undefined; "index.references.pageName": {} | undefined; "index.references.subtitle": {} | undefined; "index.veille.pageName": {} | undefined; "index.veille.subtitle": {} | undefined; "index.title": {} | undefined; "index.subtitle": {} | undefined; "index.quoi": {} | undefined; "index.comment": {} | undefined; "index.opensource": {} | undefined; "index.writing": {} | undefined; "index.latestProjects": {} | undefined; "index.latestArticles": {} | undefined; "index.allProjects": {} | undefined; "index.allArticles": {} | undefined; "index.latestSnippets": {} | undefined; "index.allSnippets": {} | undefined; "index.toc": {} | undefined; "contact.contenuVide": {} | undefined; }
declare module "astro-i18n" {
export * from "astro-i18n/"
export function l<Uri extends RouteUri>(
route: Uri | string & {},
...args: Uri extends keyof RouteParams
? undefined extends RouteParams[Uri]
? [params?: Record<string, string>, targetLangCode?: LangCode, routeLangCode?: LangCode]
: [params: RouteParams[Uri], targetLangCode?: LangCode, routeLangCode?: LangCode]
: [params?: Record<string, string>, targetLangCode?: LangCode, routeLangCode?: LangCode]
): string
export function t<Path extends TranslationPath>(
path: Path | string & {},
...args: undefined extends TranslationOptions[Path]
? [options?: keyof TranslationOptions extends Path ? Record<string, unknown> : TranslationOptions[Path], langCode?: LangCode]
: [options: TranslationOptions[Path], langCode?: LangCode]
): string
export function extractRouteLangCode(route: string): LangCode | undefined
type Translation = string | { [translationKey: string]: string | Translation }
type Translations = { [langCode: string]: Record<string, Translation> }
type RouteTranslations = { [langCode: string]: Record<string, string> }
type InterpolationFormatter = (value: unknown, ...args: unknown[]) => string
class AstroI18n {
defaultLangCode: DefaultLangCode
supportedLangCodes: SupportedLangCode[]
showDefaultLangCode: boolean
translations: Translations
routeTranslations: RouteTranslations
get langCodes(): LangCode[]
get langCode(): LangCode
set langCode(langCode: LangCode)
get formatters(): Record<string, InterpolationFormatter>
init(Astro: { url: URL }, formatters?: Record<string, InterpolationFormatter>): void
addTranslations(translations: Translations): void
addRouteTranslations(routeTranslations: RouteTranslations): void
getFormatter(name: string): InterpolationFormatter | undefined
setFormatter(name: string, formatter: InterpolationFormatter): void
deleteFormatter(name: string): void
}
export const astroI18n: AstroI18n
}

2
.prettierignore Normal file
View File

@ -0,0 +1,2 @@
**/*.d.ts
eslintrc-auto-import.mjs

34
.prettierrc.mjs Normal file
View File

@ -0,0 +1,34 @@
/** @type {import("prettier").Config} */
const config = {
arrowParens: 'always',
bracketSameLine: false,
bracketSpacing: true,
embeddedLanguageFormatting: 'auto',
endOfLine: 'lf',
htmlWhitespaceSensitivity: 'css',
insertPragma: false,
jsxSingleQuote: true,
printWidth: 80,
proseWrap: 'preserve',
quoteProps: 'as-needed',
requirePragma: false,
semi: false,
singleAttributePerLine: false,
singleQuote: true,
tabWidth: 2,
trailingComma: 'none',
useTabs: true,
vueIndentScriptAndStyle: false,
plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-astro'],
overrides: [
{
files: '*.astro',
options: {
parser: 'astro'
}
}
]
}
export default config

11
.vscode/launch.json vendored
View File

@ -1,11 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

22
.vscode/settings.json vendored
View File

@ -1,17 +1,7 @@
{
"cssvar.files": [
"./node_modules/open-props/open-props.min.css",
// if you have an alternative path to where your styles are located
// you can add it in this array of files
"assets/styles/variables.css"
],
// Do not ignore node_modules css files, which is ignored by default
"cssvar.ignore": [],
// add support for autocomplete in JS or JS like files
"cssvar.extensions": [
"css", "jsx", "tsx"
],
"editor.formatOnSave": true
}
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.insertSpaces": false,
"editor.detectIndentation": false,
"editor.tabSize": 2
}

View File

@ -9,6 +9,10 @@ export default defineConfig({
site: "https://www.nardu.in",
build: {
format: "directory",
},
i18n: {
locales: ["fr", "en"],
defaultLocale: "fr",
},
image: {
domains: ["assets.nardu.in"],

View File

@ -1,20 +0,0 @@
import { defineAstroI18nConfig } from "astro-i18n";
export default defineAstroI18nConfig({
defaultLangCode: "fr",
supportedLangCodes: ["en"],
showDefaultLangCode: false,
trailingSlash: "never",
translations: {
fr: "src/i18n/fr.json",
en: "src/i18n/en.json",
},
routeTranslations: {
en: {
"sci-hub-blocage": "sci-hub-unblock",
fragments: "snippets",
"plan-du-site": "sitemap",
references: "work",
},
},
});

BIN
bun.lockb

Binary file not shown.

View File

@ -16,11 +16,15 @@
"@astrojs/mdx": "4.0.3",
"@astrojs/rss": "4.0.10",
"@astrojs/sitemap": "3.2.1",
"@astrojs/ts-plugin": "^1.10.4",
"astro": "5.1.1",
"sharp": "^0.33.4"
"sharp": "^0.33.5"
},
"devDependencies": {
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38"
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"prettier": "^3.4.2",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-organize-imports": "^4.1.0"
}
}

View File

@ -1,38 +1,12 @@
---
import { l, astroI18n } from "astro-i18n";
// get all the locales available on the website and remove the one currently in use
const availableLocales = astroI18n.langCodes.filter(
(locale) => locale !== astroI18n.langCode
);
// current path
const currentRoute = Astro.url.pathname;
function localeName(locale) {
let localeName = "";
switch (locale) {
case "fr":
localeName = "Français";
break;
case "en":
localeName = "English";
break;
}
return localeName;
}
import { languages } from '../i18n/ui'
---
<ul role="list">
<ul>
{
// create a list of available alternative locale
availableLocales.map((locale) => (
Object.entries(languages).map(([lang, label]) => (
<li>
<a
href={l(currentRoute as any, {}, locale as any)}
class="clean-link nice-link"
>
{localeName(locale)}
</a>
<a href={`/${lang}/`}>{label}</a>
</li>
))
}

150
src/i18n/ui.ts Normal file
View File

@ -0,0 +1,150 @@
export const languages = {
en: 'English',
fr: 'Français'
}
export const defaultLang = 'fr'
export const ui = {
en: {
accueil: 'home',
tagline: 'Freelance web developer specialized in accessibility.',
copyright: '(re)Made with Astro',
contact: {
title: 'contact',
email: 'Send me an email (open in application).',
tel: 'Call or text me (open in application).',
contenuVide: 'Soon: really nice content.'
},
header: {
skipLink: 'Skip to content',
mainNav: 'Main menu',
homeLink: 'Back to homepage'
},
sitemap: 'Site map',
prevNext: {
contenus: 'Similar content',
precedent: 'Previous',
suivant: 'Next'
},
article: {
titre: 'articles',
tagline: 'I blog, sometimes.',
published: 'Published on {datetime|date(options)}'
},
meta: {
publication: 'Published on',
modification: 'Last updated on',
credit: 'Image by'
},
fragments: {
titre: 'snippets',
tagline: 'School with Nicool.'
},
references: {
titre: 'work',
slug: 'work',
cta: 'Visit website',
tagline: 'Some work.'
},
veille: {
titre: 'Around the web',
tagline: 'Some links that interested me.'
},
erreur: {
introuvable: 'Sorry, page not found.',
autre: 'Oups… sorry about that.',
lienRetour: 'Back to the home page'
},
seo: {
meta: {
description:
'Web developer specialized in accessibility and eco-design in Toulouse, France. Development of custom websites, RGAA compliance, maintenance, etc.'
},
article: {
title: 'Articles',
description:
'A few articles about graphic design and front-end development.'
},
code: {
title: 'Snippets',
description: 'Snippets of fresh, easy and accessible code.'
},
references: {
title: 'Work',
description: 'A few case studies I worked on as a front-end developer.'
}
}
},
fr: {
accueil: 'accueil',
tagline: 'Développeur web spécialisé en accessibilité.',
copyright: '(re)Fait avec Astro',
contact: {
title: 'contact',
email: 'Envoyez-moi un mail (ouverture du logiciel automatique).',
tel: 'Contactez-moi par téléphone (ouverture du logiciel automatique).'
},
contenuVide: 'Bientôt ici : du contenu de qualité',
header: {
skipLink: 'Accéder au contenu',
mainNav: 'Menu principal',
homeLink: 'Accueil du site'
},
sitemap: 'Plan du site',
prevNext: {
contenus: 'Contenus similaires',
precedent: 'Précédent',
suivant: 'Suivant'
},
article: {
titre: 'articles',
tagline: 'Je blog, un peu.',
published: 'Publié le {datetime|date(options)}'
},
meta: {
publication: 'Publié le',
modification: 'Mis à jour le',
credit: 'Image par'
},
fragments: {
titre: 'fragments',
tagline: 'Les tutos de Nico mdr.'
},
references: {
titre: 'références',
slug: 'references',
cta: 'Consulter le site',
tagline: 'Quelques références.'
},
veille: {
titre: 'veille',
tagline: 'Des liens, en vrac.'
},
erreur: {
introuvable: 'Page introuvable',
autre: 'Oups… désolé pour cette erreur.',
lienRetour: 'Retour à laccueil'
},
seo: {
meta: {
description:
'Développeur web spécialisé en accessibilité numérique et éco-conception à Toulouse. Création de sites web sur mesure, mise en conformité RGAA, maintenance, etc.'
},
article: {
title: 'Articles',
description:
"Quelques articles sur le développement web front-end et l'informatique à Toulouse."
},
code: {
title: 'Fragments',
description: 'Fragments de codes stylés, faciles et accessibles.'
},
references: {
title: 'Références',
description:
'Quelques travaux réalisés en tant que et développeur web front-end à Toulouse.'
}
}
}
} as const

View File

@ -1,6 +1,5 @@
---
import { astroI18n } from "astro-i18n";
astroI18n.init(Astro);
const locale = Astro.currentLocale;
import "../styles/style.css";
@ -11,7 +10,7 @@ import Footer from "../components/Footer.astro";
const { pageTitle } = Astro.props;
---
<html lang={astroI18n.langCode} dir="ltr">
<html lang={locale} dir="ltr">
<Head pageTitle={pageTitle} />
<body>
<div class="wrapper">

View File

@ -1,33 +1,27 @@
---
import { l, t, astroI18n } from "astro-i18n";
// import AstroImage from "../components/AstroImage.astro";
import BaseLayout from "../layouts/BaseLayout.astro";
import BaseLayout from '../layouts/BaseLayout.astro'
import { getLangFromUrl, useTranslations } from '../utils/i18n'
const { frontmatter, image, title } = Astro.props;
const publishedDate = new Date(frontmatter.pubDate);
const localizedDate = new Intl.DateTimeFormat(astroI18n.langCode, {
dateStyle: "long",
}).format(publishedDate);
const locale = getLangFromUrl(Astro.url)
const t = useTranslations(locale)
const { frontmatter, image, title } = Astro.props
const publishedDate = new Date(frontmatter.pubDate)
const localizedDate = new Intl.DateTimeFormat(locale, {
dateStyle: 'long'
}).format(publishedDate)
---
<BaseLayout pageTitle={title}>
<p>
Publié le&nbsp;: <time datetime={frontmatter.pubDate}>
{t('article.published')}&nbsp;: <time datetime={frontmatter.pubDate}>
{localizedDate}.
</time>
</p>
<p>
{
t("article.published", {
datetime: frontmatter.pubDate,
options: { dateStyle: "long" },
})
}
</p>
<div class="tags">
<div class='tags'>
{
frontmatter.tags.map((tag) => (
<p class="tag">
<p class='tag'>
<a href={l(`/tags/${[tag]}`)}>{tag}</a>
</p>
))

View File

@ -1,8 +1,5 @@
---
// init i18n
import { getLocale } from "astro-i18n-aut";
const locale = getLocale(Astro.url);
const locale = Astro.currentLocale;
// import stuff
import BaseLayout from "../layouts/BaseLayout.astro";

28
src/utils/i18n.ts Normal file
View File

@ -0,0 +1,28 @@
import { defaultLang, ui } from '../i18n/ui'
export function getLangFromUrl(url: URL) {
const [, lang] = url.pathname.split('/')
if (lang in ui) return lang as keyof typeof ui
return defaultLang
}
type NestedKeyOf<T> = {
[K in keyof T]: T[K] extends object
? `${K & string}.${NestedKeyOf<T[K]> & string}`
: K & string
}[keyof T]
export function useTranslations(lang: keyof typeof ui) {
return function t(key: NestedKeyOf<(typeof ui)[typeof defaultLang]>) {
const keys = key.split('.')
let value = ui[lang]
let fallback = ui[defaultLang]
for (const k of keys) {
value = value?.[k]
fallback = fallback?.[k]
}
return value || fallback
}
}

View File

@ -1,8 +1,18 @@
{
"extends": "astro/tsconfigs/base",
"include": [".astro/types.d.ts", "**/*"],
"extends": "astro/tsconfigs/base",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"strictNullChecks": true
}
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@components/*": ["src/components/*"],
"@layouts/*": ["src/layouts/*"]
},
"strictNullChecks": true,
"plugins": [
{
"name": "@astrojs/ts-plugin"
}
]
}
}