From e0ffa2037a19605c42091f9560e63a0ea7411d80 Mon Sep 17 00:00:00 2001 From: Ayres Vitor Date: Fri, 21 Aug 2026 13:52:41 -0300 Subject: [PATCH 1/3] feat: add prettier with astro plugin, editorconfig and LF line endings --- .editorconfig | 12 ++++++++ .gitattributes | 5 +++ .prettierignore | 8 +++++ .prettierrc | 17 +++++++++++ .vscode/extensions.json | 2 +- package-lock.json | 67 +++++++++++++++++++++++++++++++++++++++++ package.json | 6 +++- 7 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f15441a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7dbf847 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto eol=lf +*.jpg binary +*.png binary +*.webp binary +*.ico binary diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..889bfbc --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +dist/ +.astro/ +node_modules/ +package-lock.json +public/admin/ +scripts/deleted/ +.tours/ +CHANGELOG.md diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..33e8a69 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,17 @@ +{ + "useTabs": false, + "tabWidth": 2, + "endOfLine": "lf", + "printWidth": 100, + "plugins": [ + "prettier-plugin-astro" + ], + "overrides": [ + { + "files": "*.astro", + "options": { + "parser": "astro" + } + } + ] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 22a1505..06495e0 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,4 +1,4 @@ { - "recommendations": ["astro-build.astro-vscode"], + "recommendations": ["astro-build.astro-vscode", "esbenp.prettier-vscode"], "unwantedRecommendations": [] } diff --git a/package-lock.json b/package-lock.json index 9085486..b44d4c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "less": "^4.6.4" }, "devDependencies": { + "prettier": "^3.9.6", + "prettier-plugin-astro": "^0.14.1", "slugify": "^1.6.9" } }, @@ -4520,6 +4522,44 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-astro": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.14.1.tgz", + "integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^2.9.1", + "prettier": "^3.0.0", + "sass-formatter": "^0.7.6" + }, + "engines": { + "node": "^14.15.0 || >=16.0.0" + } + }, + "node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.1.tgz", + "integrity": "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==", + "dev": true, + "license": "MIT" + }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -4871,12 +4911,29 @@ "fsevents": "~2.3.2" } }, + "node_modules/s.color": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/s.color/-/s.color-0.0.15.tgz", + "integrity": "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==", + "dev": true, + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sass-formatter": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.7.9.tgz", + "integrity": "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "suf-log": "^2.5.3" + } + }, "node_modules/sax": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", @@ -5066,6 +5123,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/suf-log": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz", + "integrity": "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==", + "dev": true, + "license": "MIT", + "dependencies": { + "s.color": "0.0.15" + } + }, "node_modules/svgo": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", diff --git a/package.json b/package.json index deb73f4..82ae340 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "preview": "astro preview", "astro": "astro", "create-page": "node scripts/create-page.js", - "setup-project": "node scripts/setup-project.js" + "setup-project": "node scripts/setup-project.js", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "dependencies": { "@astrojs/sitemap": "^3.7.2", @@ -19,6 +21,8 @@ "less": "^4.6.4" }, "devDependencies": { + "prettier": "^3.9.6", + "prettier-plugin-astro": "^0.14.1", "slugify": "^1.6.9" } } From bad1cf9e98ee960bd544d1063d4872aec4cecbf2 Mon Sep 17 00:00:00 2001 From: Ayres Vitor Date: Fri, 21 Aug 2026 15:17:45 -0300 Subject: [PATCH 2/3] chore: format codebase with prettier --- .prettierrc | 4 +- LICENSE.md | 142 +- README.md | 152 +- astro.config.ts | 64 +- public/assets/favicons/site.webmanifest | 2 +- scripts/config-i18n.js | 1430 +++++++++-------- scripts/create-page.js | 474 +++--- scripts/remove-dark-mode.js | 243 ++- scripts/remove-decap.js | 419 ++--- scripts/remove-demo.js | 341 ++-- scripts/remove-i18n.js | 250 ++- scripts/setup-project.js | 253 ++- scripts/utils/collect-files.js | 18 +- scripts/utils/feature-flags.js | 78 +- scripts/utils/prompt.js | 16 +- scripts/utils/read-i18n-config.js | 26 +- scripts/utils/replace-in-files.js | 54 +- scripts/utils/transforms.js | 138 +- src/assets/js/nav.js | 187 +-- src/components/CSPicture/CSPicture.astro | 73 +- src/components/Footer/Footer.astro | 707 ++++---- src/components/Header/DynamicHeader.astro | 47 +- src/components/Meta/Meta.astro | 109 +- src/components/Select/Select.astro | 199 ++- src/content.config.ts | 48 +- src/content/blog/en/fourth-post-in-english.md | 9 +- src/content/blog/en/second-post-in-english.md | 8 +- src/content/blog/en/third-post-in-english.md | 9 +- .../blog/fr/deuxieme-article-en-francais.md | 8 +- .../blog/fr/quatrieme-article-en-francais.md | 9 +- .../blog/fr/troisieme-article-en-francais.md | 9 +- src/data/client.ts | 34 +- src/data/siteConfig.ts | 10 +- src/env.d.ts | 2 +- src/features/darkmode/ThemeSelect.astro | 28 +- src/features/decapCMS/BlogFullArticle.astro | 210 ++- src/features/decapCMS/FeaturedPost.astro | 5 +- src/features/decapCMS/Pagination.astro | 10 +- src/features/decapCMS/PaginationButton.astro | 217 ++- .../decapCMS/core/blogPostingSchema.js | 54 +- src/features/decapCMS/core/blogUtils.ts | 9 +- src/features/demo/Banner.astro | 158 +- src/features/demo/Hero.astro | 5 +- src/features/demo/Reviews.astro | 16 +- src/features/demo/SideBySide.astro | 4 +- src/features/demo/Testimonials.astro | 16 +- .../LanguageSwitch/TwoLocalesSelect.astro | 16 +- .../generateDynamicRouteTranslations.ts | 4 +- src/features/i18n/routeTranslations.ts | 12 +- .../routing/getLocalizedPathname.no-blog.ts | 9 +- .../i18n/routing/getLocalizedPathname.ts | 9 +- src/js/getSiteContext.ts | 5 +- src/js/localBusinessSchema.js | 44 +- src/layouts/BaseLayout.astro | 9 +- src/locales/en/blog.json | 14 +- src/locales/en/common.json | 124 +- src/locales/en/contact.json | 30 +- src/locales/en/home.json | 140 +- src/locales/en/reviews.json | 72 +- src/locales/fr/blog.json | 14 +- src/locales/fr/common.json | 124 +- src/locales/fr/contact.json | 30 +- src/locales/fr/home.json | 140 +- src/locales/fr/reviews.json | 72 +- src/pages/404.astro | 4 +- src/pages/about.astro | 529 +++--- src/pages/admin.astro | 102 +- src/pages/blog/[...page].astro | 584 ++++--- src/pages/blog/[...slug].astro | 6 +- src/pages/contact.astro | 32 +- src/pages/fr/404.astro | 4 +- src/pages/fr/a-propos.astro | 5 +- src/pages/fr/blog/[...page].astro | 584 ++++--- src/pages/fr/blog/[...slug].astro | 6 +- src/pages/fr/contact.astro | 32 +- src/pages/fr/index.astro | 5 +- src/pages/fr/projets/projet-1.astro | 5 +- src/pages/fr/projets/projet-2.astro | 5 +- src/pages/index.astro | 5 +- src/pages/projects/project-1.astro | 5 +- src/pages/projects/project-2.astro | 5 +- src/styles/markdown.less | 240 ++- src/styles/root.less | 416 ++--- tsconfig.json | 40 +- 84 files changed, 4799 insertions(+), 4987 deletions(-) diff --git a/.prettierrc b/.prettierrc index 33e8a69..63885d5 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,9 +3,7 @@ "tabWidth": 2, "endOfLine": "lf", "printWidth": 100, - "plugins": [ - "prettier-plugin-astro" - ], + "plugins": ["prettier-plugin-astro"], "overrides": [ { "files": "*.astro", diff --git a/LICENSE.md b/LICENSE.md index 1625c17..cb9b058 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -38,84 +38,84 @@ terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: + protected by copyright and related or neighboring rights ("Copyright and + Related Rights"). Copyright and Related Rights include, but are not + limited to, the following: - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); +i. the right to reproduce, adapt, distribute, perform, display, +communicate, and translate a Work; +ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and +likeness depicted in a Work; +iv. rights protecting against unfair competition in regards to a Work, +subject to the limitations in paragraph 4(a), below; +v. rights protecting the extraction, dissemination, use and reuse of data +in a Work; +vi. database rights (such as those arising under Directive 96/9/EC of the +European Parliament and of the Council of 11 March 1996 on the legal +protection of databases, and under any national implementation +thereof, including any amended or successor version of such +directive); and vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. +world based on applicable law or treaty, and any national +implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. + of, applicable law, Affirmer hereby overtly, fully, permanently, + irrevocably and unconditionally waives, abandons, and surrenders all of + Affirmer's Copyright and Related Rights and associated claims and causes + of action, whether now known or unknown (including existing as well as + future claims and causes of action), in the Work (i) in all territories + worldwide, (ii) for the maximum duration provided by applicable law or + treaty (including future time extensions), (iii) in any current or future + medium and for any number of copies, and (iv) for any purpose whatsoever, + including without limitation commercial, advertising or promotional + purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each + member of the public at large and to the detriment of Affirmer's heirs and + successors, fully intending that such Waiver shall not be subject to + revocation, rescission, cancellation, termination, or any other legal or + equitable action to disrupt the quiet enjoyment of the Work by the public + as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. + be judged legally invalid or ineffective under applicable law, then the + Waiver shall be preserved to the maximum extent permitted taking into + account Affirmer's express Statement of Purpose. In addition, to the + extent the Waiver is so judged Affirmer hereby grants to each affected + person a royalty-free, non transferable, non sublicensable, non exclusive, + irrevocable and unconditional license to exercise Affirmer's Copyright and + Related Rights in the Work (i) in all territories worldwide, (ii) for the + maximum duration provided by applicable law or treaty (including future + time extensions), (iii) in any current or future medium and for any number + of copies, and (iv) for any purpose whatsoever, including without + limitation commercial, advertising or promotional purposes (the + "License"). The License shall be deemed effective as of the date CC0 was + applied by Affirmer to the Work. Should any part of the License for any + reason be judged legally invalid or ineffective under applicable law, such + partial invalidity or ineffectiveness shall not invalidate the remainder + of the License, and in such case Affirmer hereby affirms that he or she + will not (i) exercise any of his or her remaining Copyright and Related + Rights in the Work or (ii) assert any associated claims and causes of + action with respect to the Work, in either case contrary to Affirmer's + express Statement of Purpose. 4. Limitations and Disclaimers. - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. \ No newline at end of file +a. No trademark or patent rights held by Affirmer are waived, abandoned, +surrendered, licensed or otherwise affected by this document. +b. Affirmer offers the Work as-is and makes no representations or +warranties of any kind concerning the Work, express, implied, +statutory or otherwise, including without limitation warranties of +title, merchantability, fitness for a particular purpose, non +infringement, or the absence of latent or other defects, accuracy, or +the present or absence of errors, whether or not discoverable, all to +the greatest extent permissible under applicable law. +c. Affirmer disclaims responsibility for clearing rights of other persons +that may apply to the Work or any use thereof, including without +limitation any person's Copyright and Related Rights in the Work. +Further, Affirmer disclaims responsibility for obtaining any necessary +consents, permissions or other rights required for any use of the +Work. +d. Affirmer understands and acknowledges that Creative Commons is not a +party to this document and has no duty or obligation with respect to +this CC0 or use of the Work. diff --git a/README.md b/README.md index 88ec95e..017bc94 100644 --- a/README.md +++ b/README.md @@ -11,38 +11,38 @@ - [Overview](#overview) - [Getting Started](#getting-started) - - [Using the Github template](#using-the-github-template) - - [Using the CLI](#using-the-cli) - - [Quickstart](#quickstart) - - [Set up your project](#set-up-your-project) - - [Essential files to configure](#essential-files-to-configure) - - [Commands](#commands) + - [Using the Github template](#using-the-github-template) + - [Using the CLI](#using-the-cli) + - [Quickstart](#quickstart) + - [Set up your project](#set-up-your-project) + - [Essential files to configure](#essential-files-to-configure) + - [Commands](#commands) - [Features](#features) - [Project Structure](#project-structure) - - [Project Tree](#project-tree) - - [Key Directories](#key-directories) + - [Project Tree](#project-tree) + - [Key Directories](#key-directories) - [i18n System](#i18n-system) - - [Overview and Config](#overview-and-config) - - [Adding or changing locales](#adding-or-changing-locales) - - [Page Structure](#page-structure) - - [Configuration Files](#configuration-files) - - [Translation Files and Namespaces](#translation-files-and-namespaces) - - [Using Translations](#using-translations) - - [Generating Localized URLs](#generating-localized-urls) - - [Localizing Route Slugs](#localizing-route-slugs) - - [Localizing Blog Post Slugs](#localizing-blog-post-slugs) - - [Language Switcher Components](#language-switcher-components) + - [Overview and Config](#overview-and-config) + - [Adding or changing locales](#adding-or-changing-locales) + - [Page Structure](#page-structure) + - [Configuration Files](#configuration-files) + - [Translation Files and Namespaces](#translation-files-and-namespaces) + - [Using Translations](#using-translations) + - [Generating Localized URLs](#generating-localized-urls) + - [Localizing Route Slugs](#localizing-route-slugs) + - [Localizing Blog Post Slugs](#localizing-blog-post-slugs) + - [Language Switcher Components](#language-switcher-components) - [Content Management & Blog](#content-management--blog) - - [Content Collections](#content-collections) - - [Configuring the CMS](#configuring-the-cms) - - [i18n Blog Structure](#i18n-blog-structure) - - [Accessing the Dashboard](#accessing-the-dashboard) - - [Featured Posts](#featured-posts) - - [Styling the Preview Pane](#styling-the-preview-pane) - - [Local Backend Setup](#local-backend-setup) + - [Content Collections](#content-collections) + - [Configuring the CMS](#configuring-the-cms) + - [i18n Blog Structure](#i18n-blog-structure) + - [Accessing the Dashboard](#accessing-the-dashboard) + - [Featured Posts](#featured-posts) + - [Styling the Preview Pane](#styling-the-preview-pane) + - [Local Backend Setup](#local-backend-setup) - [Deployment](#deployment) - - [Pre-Deployment Checklist](#pre-deployment-checklist) - - [Setting Up Decap CMS with DecapBridge](#setting-up-decap-cms-with-decapbridge) + - [Pre-Deployment Checklist](#pre-deployment-checklist) + - [Setting Up Decap CMS with DecapBridge](#setting-up-decap-cms-with-decapbridge) - [Acknowledgments](#acknowledgments) - [Conclusion](#conclusion) @@ -107,15 +107,15 @@ This is the main onboarding command: it asks which optional features to keep (i1 Once you've run `setup-project`, these are the files most projects need to personalize before writing any new code: -| File | What to update | -| ---- | --------------- | -| `src/data/client.ts` | Business name, email, phone, address (`BUSINESS` object) | -| `src/data/siteConfig.ts` | Domain, description, social share image (`SITE`, `OG`) | -| `astro.config.ts` | `site` — your production domain | -| `src/styles/root.less` | Brand colors/fonts via CSS variables (`--primary`, `--secondary`, `--headerColor`, etc.) | -| `src/components/Settings/Settings.astro` | Swap or remove the dark-mode toggle / language switcher | -| `src/data/navData.json` | Nav links, and per-locale translated paths if i18n is kept | -| `public/admin/config.yml` | Decap CMS repo + DecapBridge auth endpoints, if keeping the CMS | +| File | What to update | +| ---------------------------------------- | ---------------------------------------------------------------------------------------- | +| `src/data/client.ts` | Business name, email, phone, address (`BUSINESS` object) | +| `src/data/siteConfig.ts` | Domain, description, social share image (`SITE`, `OG`) | +| `astro.config.ts` | `site` — your production domain | +| `src/styles/root.less` | Brand colors/fonts via CSS variables (`--primary`, `--secondary`, `--headerColor`, etc.) | +| `src/components/Settings/Settings.astro` | Swap or remove the dark-mode toggle / language switcher | +| `src/data/navData.json` | Nav links, and per-locale translated paths if i18n is kept | +| `public/admin/config.yml` | Decap CMS repo + DecapBridge auth endpoints, if keeping the CMS | See [Pre-Deployment Checklist](#pre-deployment-checklist) for the full list to double-check right before going live (production domain, favicons, sitemap, etc.). @@ -123,19 +123,19 @@ See [Pre-Deployment Checklist](#pre-deployment-checklist) for the full list to d All commands are run from the root of the project, from a terminal: -| Command | Action | -| ----------------------- | -------------------------------------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run setup-project` | Interactively choose which features to keep/remove, then configure locales | -| `npm run create-page` | Scaffolds a new page for all locales | -| `node scripts/config-i18n.js` | Reconfigure locales interactively (default locale, additional locales, URL prefixing) | -| `node scripts/remove-i18n.js` | Permanently removes the i18n system | -| `node scripts/remove-decap.js` | Removes Decap CMS integration | -| `node scripts/remove-demo.js` | Removes demo/placeholder content | -| `node scripts/remove-dark-mode.js` | Removes dark mode components and styles | +| Command | Action | +| ---------------------------------- | ------------------------------------------------------------------------------------- | +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:4321` | +| `npm run build` | Build your production site to `./dist/` | +| `npm run preview` | Preview your build locally, before deploying | +| `npm run setup-project` | Interactively choose which features to keep/remove, then configure locales | +| `npm run create-page` | Scaffolds a new page for all locales | +| `node scripts/config-i18n.js` | Reconfigure locales interactively (default locale, additional locales, URL prefixing) | +| `node scripts/remove-i18n.js` | Permanently removes the i18n system | +| `node scripts/remove-decap.js` | Removes Decap CMS integration | +| `node scripts/remove-demo.js` | Removes demo/placeholder content | +| `node scripts/remove-dark-mode.js` | Removes dark mode components and styles | ## Features @@ -312,12 +312,12 @@ JSON files for each locale must have the **same structure and keys** — only th ```json { - "ctaComponent": { - "title": "Get It Done", - "subtitle": "With Us Today", - "message": "Say something encouraging...", - "cta": "Get a Quote" - } + "ctaComponent": { + "title": "Get It Done", + "subtitle": "With Us Today", + "message": "Say something encouraging...", + "cta": "Get a Quote" + } } ``` @@ -325,12 +325,12 @@ JSON files for each locale must have the **same structure and keys** — only th ```json { - "ctaComponent": { - "title": "Confiez votre projet", - "subtitle": "à nos experts", - "message": "Dites quelque chose d'accrocheur...", - "cta": "Obtenir un devis" - } + "ctaComponent": { + "title": "Confiez votre projet", + "subtitle": "à nos experts", + "message": "Dites quelque chose d'accrocheur...", + "cta": "Obtenir un devis" + } } ``` @@ -378,9 +378,9 @@ Adding a page with a translated slug is a 3-step combo: ```json { - "key": "my-page", - "urls": { "en": "/my-page", "fr": "/ma-page" }, - "label": { "en": "My Page", "fr": "Ma Page" } + "key": "my-page", + "urls": { "en": "/my-page", "fr": "/ma-page" }, + "label": { "en": "My Page", "fr": "Ma Page" } } ``` @@ -455,9 +455,9 @@ The CMS mirrors this kit's bilingual blog structure. In `config.yml`, the i18n b ```yaml i18n: - structure: multiple_folders - locales: [en, fr] - default_locale: en + structure: multiple_folders + locales: [en, fr] + default_locale: en ``` This maps to `src/content/blog/en/` and `src/content/blog/fr/` on disk. When an editor creates a post, Decap saves language variants into the corresponding locale folder automatically. @@ -488,7 +488,7 @@ To run Decap CMS locally without deploying (useful for content entry during deve ```yaml local_backend: true backend: - # ... rest of your backend config + # ... rest of your backend config ``` 2. Install the required packages: @@ -569,14 +569,14 @@ DecapBridge will generate a backend configuration snippet. This kit is pre-confi ```yaml # PKCE format (used in this kit) backend: - name: git-gateway - repo: your-github-username/your-repo-name - branch: main - auth_type: pkce - base_url: https://auth.decapbridge.com - auth_endpoint: /sites//pkce - auth_token_endpoint: /sites//token - gateway_url: https://gateway.decapbridge.com + name: git-gateway + repo: your-github-username/your-repo-name + branch: main + auth_type: pkce + base_url: https://auth.decapbridge.com + auth_endpoint: /sites//pkce + auth_token_endpoint: /sites//token + gateway_url: https://gateway.decapbridge.com ``` > [!NOTE] diff --git a/astro.config.ts b/astro.config.ts index 9a5b080..b4bf11e 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -4,36 +4,36 @@ import sitemap from "@astrojs/sitemap"; import { SITE } from "./src/data/siteConfig"; export default defineConfig({ - site: SITE.url, - i18n: { - defaultLocale: "en", - locales: ["en", "fr"], - routing: { - prefixDefaultLocale: false, - }, - }, - trailingSlash: "always", - integrations: [ - icon(), - sitemap({ - filter: (page) => !page.includes("/admin"), - i18n: { - defaultLocale: "en", - locales: { - en: "en-US", - fr: "fr-FR", - }, - }, - }), - ], - fonts: [ - { - provider: fontProviders.fontsource(), - name: "Roboto", - cssVariable: "--font-primary", - fallbacks: ["Arial", "sans-serif"], - weights: [400, 700, 900], - styles: ["normal"], - }, - ], + site: SITE.url, + i18n: { + defaultLocale: "en", + locales: ["en", "fr"], + routing: { + prefixDefaultLocale: false, + }, + }, + trailingSlash: "always", + integrations: [ + icon(), + sitemap({ + filter: (page) => !page.includes("/admin"), + i18n: { + defaultLocale: "en", + locales: { + en: "en-US", + fr: "fr-FR", + }, + }, + }), + ], + fonts: [ + { + provider: fontProviders.fontsource(), + name: "Roboto", + cssVariable: "--font-primary", + fallbacks: ["Arial", "sans-serif"], + weights: [400, 700, 900], + styles: ["normal"], + }, + ], }); diff --git a/public/assets/favicons/site.webmanifest b/public/assets/favicons/site.webmanifest index 3ba4373..2445a1c 100644 --- a/public/assets/favicons/site.webmanifest +++ b/public/assets/favicons/site.webmanifest @@ -18,4 +18,4 @@ "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" -} \ No newline at end of file +} diff --git a/scripts/config-i18n.js b/scripts/config-i18n.js index a943028..aa4e62d 100644 --- a/scripts/config-i18n.js +++ b/scripts/config-i18n.js @@ -13,198 +13,225 @@ const root = process.env.SCRIPT_ROOT ?? join(__dirname, ".."); // ─── Guard ──────────────────────────────────────────────────────────────────── try { - await fs.access(join(root, ".i18n-removed")); - console.log("i18n support has already been removed from this project (.i18n-removed marker exists). Exiting."); - process.exit(0); -} catch { /* proceed */ } + await fs.access(join(root, ".i18n-removed")); + console.log( + "i18n support has already been removed from this project (.i18n-removed marker exists). Exiting.", + ); + process.exit(0); +} catch { + /* proceed */ +} // ─── Helpers ────────────────────────────────────────────────────────────────── const LOCALE_RE = /^[a-z]{2}(-[A-Z]{2})?$/; function validateLocale(l) { - return LOCALE_RE.test(l); + return LOCALE_RE.test(l); } function localeToBCP47(locale) { - const exceptions = { en: "en-US", zh: "zh-CN", pt: "pt-PT" }; - return exceptions[locale] ?? `${locale}-${locale.toUpperCase()}`; + const exceptions = { en: "en-US", zh: "zh-CN", pt: "pt-PT" }; + return exceptions[locale] ?? `${locale}-${locale.toUpperCase()}`; } function parseRecordEntries(content, varName) { - const match = content.match(new RegExp(`${varName}[^=]*=\\s*\\{([^}]+)\\}`)); - if (!match) return {}; - const result = {}; - const re = /(\w+):\s*"([^"]+)"/g; - let m; - while ((m = re.exec(match[1])) !== null) { - result[m[1]] = m[2]; - } - return result; + const match = content.match(new RegExp(`${varName}[^=]*=\\s*\\{([^}]+)\\}`)); + if (!match) return {}; + const result = {}; + const re = /(\w+):\s*"([^"]+)"/g; + let m; + while ((m = re.exec(match[1])) !== null) { + result[m[1]] = m[2]; + } + return result; } function buildRecordString(locales, existingEntries, generator) { - return "{ " + locales.map((l) => `${l}: "${existingEntries[l] ?? generator(l)}"`).join(", ") + " }"; + return ( + "{ " + locales.map((l) => `${l}: "${existingEntries[l] ?? generator(l)}"`).join(", ") + " }" + ); } // Extract the inner content of a named locale block (e.g. `en: { ... }`) function extractLocaleBlockInner(content, locale) { - const regex = new RegExp(`\\b${locale}:\\s*\\{`); - const match = regex.exec(content); - if (!match) return null; - const braceOpen = content.indexOf("{", match.index + match[0].length - 1); - let depth = 1; - let i = braceOpen + 1; - while (i < content.length && depth > 0) { - if (content[i] === "{") depth++; - else if (content[i] === "}") depth--; - i++; - } - return content.slice(braceOpen + 1, i - 1); + const regex = new RegExp(`\\b${locale}:\\s*\\{`); + const match = regex.exec(content); + if (!match) return null; + const braceOpen = content.indexOf("{", match.index + match[0].length - 1); + let depth = 1; + let i = braceOpen + 1; + while (i < content.length && depth > 0) { + if (content[i] === "{") depth++; + else if (content[i] === "}") depth--; + i++; + } + return content.slice(braceOpen + 1, i - 1); } // Insert a new locale block before the closing `};` of routeTranslations function insertLocaleBlock(content, locale, innerContent) { - const match = /export const routeTranslations[^=]*=\s*\{/.exec(content); - if (!match) return content; - const braceOpen = content.indexOf("{", match.index + match[0].length - 1); - let depth = 1; - let i = braceOpen + 1; - while (i < content.length && depth > 0) { - if (content[i] === "{") depth++; - else if (content[i] === "}") depth--; - i++; - } - const closingBrace = i - 1; - let lineStart = closingBrace; - while (lineStart > 0 && content[lineStart - 1] !== "\n") lineStart--; - const newBlock = ` ${locale}: {${innerContent.trimEnd()}\n },\n`; - return content.slice(0, lineStart) + newBlock + content.slice(lineStart); + const match = /export const routeTranslations[^=]*=\s*\{/.exec(content); + if (!match) return content; + const braceOpen = content.indexOf("{", match.index + match[0].length - 1); + let depth = 1; + let i = braceOpen + 1; + while (i < content.length && depth > 0) { + if (content[i] === "{") depth++; + else if (content[i] === "}") depth--; + i++; + } + const closingBrace = i - 1; + let lineStart = closingBrace; + while (lineStart > 0 && content[lineStart - 1] !== "\n") lineStart--; + const newBlock = ` ${locale}: {${innerContent.trimEnd()}\n },\n`; + return content.slice(0, lineStart) + newBlock + content.slice(lineStart); } // Add/remove/rename locale entries inside the localizedCollections object function modifyLocalizedCollections(content, operation, opts = {}) { - const match = /export const localizedCollections\s*=\s*\{/.exec(content); - if (!match) return content; - const braceOpen = content.indexOf("{", match.index + match[0].length - 1); - let depth = 1; - let i = braceOpen + 1; - while (i < content.length && depth > 0) { - if (content[i] === "{") depth++; - else if (content[i] === "}") depth--; - i++; - } - const blockEnd = i; - let block = content.slice(braceOpen, blockEnd); - - if (operation === "add") { - const { locale, defaultLocale } = opts; - block = block.replace(/\{([^{}]+)\}/g, (_, entries) => { - const defMatch = new RegExp(`\\b${defaultLocale}:\\s*"([^"]+)"`).exec(entries); - const val = defMatch?.[1] ?? locale; - return `{ ${entries.trimEnd()}, ${locale}: "${val}" }`; - }); - } else if (operation === "remove") { - block = block.replace(new RegExp(`,?\\s*${opts.locale}:\\s*"[^"]*"`, "g"), ""); - } else if (operation === "rename") { - block = block.replace(new RegExp(`\\b${opts.from}:`, "g"), `${opts.to}:`); - } - - return content.slice(0, braceOpen) + block + content.slice(blockEnd); + const match = /export const localizedCollections\s*=\s*\{/.exec(content); + if (!match) return content; + const braceOpen = content.indexOf("{", match.index + match[0].length - 1); + let depth = 1; + let i = braceOpen + 1; + while (i < content.length && depth > 0) { + if (content[i] === "{") depth++; + else if (content[i] === "}") depth--; + i++; + } + const blockEnd = i; + let block = content.slice(braceOpen, blockEnd); + + if (operation === "add") { + const { locale, defaultLocale } = opts; + block = block.replace(/\{([^{}]+)\}/g, (_, entries) => { + const defMatch = new RegExp(`\\b${defaultLocale}:\\s*"([^"]+)"`).exec(entries); + const val = defMatch?.[1] ?? locale; + return `{ ${entries.trimEnd()}, ${locale}: "${val}" }`; + }); + } else if (operation === "remove") { + block = block.replace(new RegExp(`,?\\s*${opts.locale}:\\s*"[^"]*"`, "g"), ""); + } else if (operation === "rename") { + block = block.replace(new RegExp(`\\b${opts.from}:`, "g"), `${opts.to}:`); + } + + return content.slice(0, braceOpen) + block + content.slice(blockEnd); } // ─── Determine locale operations ────────────────────────────────────────────── function determineOperations({ defaultLocale, currentLocales, newDefaultLocale, newLocales }) { - let editOldDefaultToNewDefault; - let localesToAdd, localesToRemove; - - if (currentLocales.includes(newDefaultLocale)) { - // New default already exists as a current locale - editOldDefaultToNewDefault = false; - localesToAdd = newLocales.filter((l) => !currentLocales.includes(l)); - localesToRemove = currentLocales.filter((l) => !newLocales.includes(l)); - } else if (newLocales.includes(defaultLocale)) { - // Old default is kept as a non-default locale - editOldDefaultToNewDefault = false; - localesToAdd = newLocales.filter((l) => !currentLocales.includes(l)); - localesToRemove = currentLocales.filter((l) => !newLocales.includes(l)); - } else { - // Old default is gone entirely — rename it to the new default - editOldDefaultToNewDefault = true; - localesToAdd = newLocales.filter((l) => !currentLocales.includes(l) && l !== newDefaultLocale); - localesToRemove = currentLocales.filter((l) => !newLocales.includes(l) && l !== defaultLocale); - } - - return { localesToAdd, localesToRemove, editOldDefaultToNewDefault }; + let editOldDefaultToNewDefault; + let localesToAdd, localesToRemove; + + if (currentLocales.includes(newDefaultLocale)) { + // New default already exists as a current locale + editOldDefaultToNewDefault = false; + localesToAdd = newLocales.filter((l) => !currentLocales.includes(l)); + localesToRemove = currentLocales.filter((l) => !newLocales.includes(l)); + } else if (newLocales.includes(defaultLocale)) { + // Old default is kept as a non-default locale + editOldDefaultToNewDefault = false; + localesToAdd = newLocales.filter((l) => !currentLocales.includes(l)); + localesToRemove = currentLocales.filter((l) => !newLocales.includes(l)); + } else { + // Old default is gone entirely — rename it to the new default + editOldDefaultToNewDefault = true; + localesToAdd = newLocales.filter((l) => !currentLocales.includes(l) && l !== newDefaultLocale); + localesToRemove = currentLocales.filter((l) => !newLocales.includes(l) && l !== defaultLocale); + } + + return { localesToAdd, localesToRemove, editOldDefaultToNewDefault }; } // ─── Phase A: astro.config.ts ──────────────────────────────────────────────── -async function patchAstroConfig({ defaultLocale, newDefaultLocale, newLocales, prefixDefaultLocale }) { - const configPath = join(root, "astro.config.ts"); - try { - let content = await fs.readFile(configPath, "utf-8"); - const localesString = newLocales.map((l) => `"${l}"`).join(", "); - content = content.replace(`defaultLocale: "${defaultLocale}"`, `defaultLocale: "${newDefaultLocale}"`); - content = content.replace(/locales:\s*\[[^\]]+\]/, `locales: [${localesString}]`); - content = content.replace(/prefixDefaultLocale:\s*(true|false)/, `prefixDefaultLocale: ${prefixDefaultLocale}`); - await fs.writeFile(configPath, content, "utf-8"); - console.log(" Patched astro.config.ts"); - } catch (err) { - console.error(` Error patching astro.config.ts: ${err.message}`); - } +async function patchAstroConfig({ + defaultLocale, + newDefaultLocale, + newLocales, + prefixDefaultLocale, +}) { + const configPath = join(root, "astro.config.ts"); + try { + let content = await fs.readFile(configPath, "utf-8"); + const localesString = newLocales.map((l) => `"${l}"`).join(", "); + content = content.replace( + `defaultLocale: "${defaultLocale}"`, + `defaultLocale: "${newDefaultLocale}"`, + ); + content = content.replace(/locales:\s*\[[^\]]+\]/, `locales: [${localesString}]`); + content = content.replace( + /prefixDefaultLocale:\s*(true|false)/, + `prefixDefaultLocale: ${prefixDefaultLocale}`, + ); + await fs.writeFile(configPath, content, "utf-8"); + console.log(" Patched astro.config.ts"); + } catch (err) { + console.error(` Error patching astro.config.ts: ${err.message}`); + } } // ─── Phase B: i18nConfig.ts ─────────────────────────────────────────────────── -async function patchSiteSettings({ defaultLocale, newDefaultLocale, newLocales, localesToAdd, localesToRemove, editOldDefaultToNewDefault }) { - const settingsPath = join(root, "src", "features", "i18n", "i18nConfig.ts"); - try { - let content = await fs.readFile(settingsPath, "utf-8"); - - // locales array - const localesString = newLocales.map((l) => `"${l}"`).join(", "); - content = content.replace(/locales\s*=\s*\[[^\]]+\]/, `locales = [${localesString}]`); - - // defaultLocale value - content = content.replace( - /defaultLocale[^=]*=\s*["'][^"']+["']/, - `defaultLocale: Locale = "${newDefaultLocale}"`, - ); - - // localeMap — parse existing values, rebuild (preserves known entries, generates defaults for new) - const existingLocaleMap = parseRecordEntries(content, "localeMap"); - const newLocaleMapStr = buildRecordString(newLocales, existingLocaleMap, localeToBCP47); - content = content.replace( - /localeMap[^=]*=\s*\{[^}]+\}/, - `localeMap: Record = ${newLocaleMapStr}`, - ); - - // languageSwitcherMap — same pattern - const existingLangMap = parseRecordEntries(content, "languageSwitcherMap"); - const newLangMapStr = buildRecordString(newLocales, existingLangMap, (l) => l.toUpperCase()); - content = content.replace( - /languageSwitcherMap[^=]*=\s*\{[^}]+\}/, - `languageSwitcherMap: Record = ${newLangMapStr}`, - ); - - // localizedCollections — add/remove/rename locale entries - for (const locale of localesToAdd) { - content = modifyLocalizedCollections(content, "add", { locale, defaultLocale }); - } - for (const locale of localesToRemove) { - content = modifyLocalizedCollections(content, "remove", { locale }); - } - if (editOldDefaultToNewDefault) { - content = modifyLocalizedCollections(content, "rename", { from: defaultLocale, to: newDefaultLocale }); - } - - await fs.writeFile(settingsPath, content, "utf-8"); - console.log(" Patched src/features/i18n/i18nConfig.ts"); - } catch (err) { - console.error(` Error patching i18nConfig.ts: ${err.message}`); - } +async function patchSiteSettings({ + defaultLocale, + newDefaultLocale, + newLocales, + localesToAdd, + localesToRemove, + editOldDefaultToNewDefault, +}) { + const settingsPath = join(root, "src", "features", "i18n", "i18nConfig.ts"); + try { + let content = await fs.readFile(settingsPath, "utf-8"); + + // locales array + const localesString = newLocales.map((l) => `"${l}"`).join(", "); + content = content.replace(/locales\s*=\s*\[[^\]]+\]/, `locales = [${localesString}]`); + + // defaultLocale value + content = content.replace( + /defaultLocale[^=]*=\s*["'][^"']+["']/, + `defaultLocale: Locale = "${newDefaultLocale}"`, + ); + + // localeMap — parse existing values, rebuild (preserves known entries, generates defaults for new) + const existingLocaleMap = parseRecordEntries(content, "localeMap"); + const newLocaleMapStr = buildRecordString(newLocales, existingLocaleMap, localeToBCP47); + content = content.replace( + /localeMap[^=]*=\s*\{[^}]+\}/, + `localeMap: Record = ${newLocaleMapStr}`, + ); + + // languageSwitcherMap — same pattern + const existingLangMap = parseRecordEntries(content, "languageSwitcherMap"); + const newLangMapStr = buildRecordString(newLocales, existingLangMap, (l) => l.toUpperCase()); + content = content.replace( + /languageSwitcherMap[^=]*=\s*\{[^}]+\}/, + `languageSwitcherMap: Record = ${newLangMapStr}`, + ); + + // localizedCollections — add/remove/rename locale entries + for (const locale of localesToAdd) { + content = modifyLocalizedCollections(content, "add", { locale, defaultLocale }); + } + for (const locale of localesToRemove) { + content = modifyLocalizedCollections(content, "remove", { locale }); + } + if (editOldDefaultToNewDefault) { + content = modifyLocalizedCollections(content, "rename", { + from: defaultLocale, + to: newDefaultLocale, + }); + } + + await fs.writeFile(settingsPath, content, "utf-8"); + console.log(" Patched src/features/i18n/i18nConfig.ts"); + } catch (err) { + console.error(` Error patching i18nConfig.ts: ${err.message}`); + } } // ─── Phase C: routeTranslations.ts ─────────────────────────────────────────── @@ -212,110 +239,125 @@ async function patchSiteSettings({ defaultLocale, newDefaultLocale, newLocales, // so this phase is a no-op on the current project structure. Route slugs for // new locales must be added manually in src/data/navData.json. -async function patchRouteTranslations({ defaultLocale, newDefaultLocale, localesToAdd, localesToRemove, editOldDefaultToNewDefault }) { - const rtPath = join(root, "src", "features", "i18n", "routeTranslations.ts"); - if (!existsSync(rtPath)) { - console.log(" Skipped routeTranslations.ts — not found"); - return; - } - try { - let content = await fs.readFile(rtPath, "utf-8"); - - // Add new locale blocks — clone default locale's slugs as identity placeholders - for (const locale of localesToAdd) { - const defaultInner = extractLocaleBlockInner(content, defaultLocale); - if (defaultInner !== null) { - content = insertLocaleBlock(content, locale, defaultInner); - } - content = modifyLocalizedCollections(content, "add", { locale, defaultLocale }); - } - - // Remove locale blocks - for (const locale of localesToRemove) { - content = removeObjectKey(content, locale); - content = modifyLocalizedCollections(content, "remove", { locale }); - } - - // Rename default locale key throughout - if (editOldDefaultToNewDefault) { - content = content.replace( - new RegExp(`(\\b)${defaultLocale}(:\\s*\\{)`, "g"), - `$1${newDefaultLocale}$2`, - ); - content = modifyLocalizedCollections(content, "rename", { from: defaultLocale, to: newDefaultLocale }); - } - - content = content.replace(/\n{3,}/g, "\n\n"); - await fs.writeFile(rtPath, content, "utf-8"); - console.log(" Patched src/features/i18n/routeTranslations.ts"); - } catch (err) { - console.error(` Error patching routeTranslations.ts: ${err.message}`); - } +async function patchRouteTranslations({ + defaultLocale, + newDefaultLocale, + localesToAdd, + localesToRemove, + editOldDefaultToNewDefault, +}) { + const rtPath = join(root, "src", "features", "i18n", "routeTranslations.ts"); + if (!existsSync(rtPath)) { + console.log(" Skipped routeTranslations.ts — not found"); + return; + } + try { + let content = await fs.readFile(rtPath, "utf-8"); + + // Add new locale blocks — clone default locale's slugs as identity placeholders + for (const locale of localesToAdd) { + const defaultInner = extractLocaleBlockInner(content, defaultLocale); + if (defaultInner !== null) { + content = insertLocaleBlock(content, locale, defaultInner); + } + content = modifyLocalizedCollections(content, "add", { locale, defaultLocale }); + } + + // Remove locale blocks + for (const locale of localesToRemove) { + content = removeObjectKey(content, locale); + content = modifyLocalizedCollections(content, "remove", { locale }); + } + + // Rename default locale key throughout + if (editOldDefaultToNewDefault) { + content = content.replace( + new RegExp(`(\\b)${defaultLocale}(:\\s*\\{)`, "g"), + `$1${newDefaultLocale}$2`, + ); + content = modifyLocalizedCollections(content, "rename", { + from: defaultLocale, + to: newDefaultLocale, + }); + } + + content = content.replace(/\n{3,}/g, "\n\n"); + await fs.writeFile(rtPath, content, "utf-8"); + console.log(" Patched src/features/i18n/routeTranslations.ts"); + } catch (err) { + console.error(` Error patching routeTranslations.ts: ${err.message}`); + } } // ─── Phase D: public/admin/config.yml ──────────────────────────────────────── async function patchDecapConfig({ newDefaultLocale, newLocales }) { - const configPath = join(root, "public", "admin", "config.yml"); - if (!existsSync(configPath)) { - console.log(" Skipped public/admin/config.yml — not found (Decap may have been removed)"); - return; - } - try { - let content = await fs.readFile(configPath, "utf-8"); - content = content.replace(/locales:\s*\[.*?\]/, `locales: [${newLocales.join(", ")}]`); - content = content.replace(/default_locale:\s*\S+/, `default_locale: ${newDefaultLocale}`); - await fs.writeFile(configPath, content, "utf-8"); - console.log(" Patched public/admin/config.yml"); - } catch (err) { - console.error(` Error patching config.yml: ${err.message}`); - } + const configPath = join(root, "public", "admin", "config.yml"); + if (!existsSync(configPath)) { + console.log(" Skipped public/admin/config.yml — not found (Decap may have been removed)"); + return; + } + try { + let content = await fs.readFile(configPath, "utf-8"); + content = content.replace(/locales:\s*\[.*?\]/, `locales: [${newLocales.join(", ")}]`); + content = content.replace(/default_locale:\s*\S+/, `default_locale: ${newDefaultLocale}`); + await fs.writeFile(configPath, content, "utf-8"); + console.log(" Patched public/admin/config.yml"); + } catch (err) { + console.error(` Error patching config.yml: ${err.message}`); + } } // ─── Phase E: src/locales/ ──────────────────────────────────────────────────── -async function patchLocalesFolders({ defaultLocale, newDefaultLocale, localesToAdd, localesToRemove, editOldDefaultToNewDefault }) { - const localesDir = join(root, "src", "locales"); - if (!existsSync(localesDir)) { - console.log(" Skipped src/locales/ — directory not found"); - return; - } - const deletedDir = join(root, "scripts", "deleted"); - - // Add new locale folders (copy from default locale — JSON keys to translate) - for (const locale of localesToAdd) { - const src = join(localesDir, defaultLocale); - const dest = join(localesDir, locale); - if (existsSync(dest)) { - console.log(` Skipped src/locales/${locale}/ — already exists`); - continue; - } - if (existsSync(src)) { - await fs.cp(src, dest, { recursive: true }); - console.log(` Created src/locales/${locale}/ (copied from src/locales/${defaultLocale}/)`); - } - } - - // Remove locale folders → move to scripts/deleted/ - for (const locale of localesToRemove) { - const src = join(localesDir, locale); - if (!existsSync(src)) continue; - await fs.mkdir(deletedDir, { recursive: true }); - const dest = join(deletedDir, `locales-${locale}`); - if (existsSync(dest)) await fs.rm(dest, { recursive: true }); - await fs.rename(src, dest); - console.log(` Moved src/locales/${locale}/ → scripts/deleted/locales-${locale}/`); - } - - // Rename default locale folder - if (editOldDefaultToNewDefault) { - const src = join(localesDir, defaultLocale); - const dest = join(localesDir, newDefaultLocale); - if (existsSync(src)) { - await fs.rename(src, dest); - console.log(` Renamed src/locales/${defaultLocale}/ → src/locales/${newDefaultLocale}/`); - } - } +async function patchLocalesFolders({ + defaultLocale, + newDefaultLocale, + localesToAdd, + localesToRemove, + editOldDefaultToNewDefault, +}) { + const localesDir = join(root, "src", "locales"); + if (!existsSync(localesDir)) { + console.log(" Skipped src/locales/ — directory not found"); + return; + } + const deletedDir = join(root, "scripts", "deleted"); + + // Add new locale folders (copy from default locale — JSON keys to translate) + for (const locale of localesToAdd) { + const src = join(localesDir, defaultLocale); + const dest = join(localesDir, locale); + if (existsSync(dest)) { + console.log(` Skipped src/locales/${locale}/ — already exists`); + continue; + } + if (existsSync(src)) { + await fs.cp(src, dest, { recursive: true }); + console.log(` Created src/locales/${locale}/ (copied from src/locales/${defaultLocale}/)`); + } + } + + // Remove locale folders → move to scripts/deleted/ + for (const locale of localesToRemove) { + const src = join(localesDir, locale); + if (!existsSync(src)) continue; + await fs.mkdir(deletedDir, { recursive: true }); + const dest = join(deletedDir, `locales-${locale}`); + if (existsSync(dest)) await fs.rm(dest, { recursive: true }); + await fs.rename(src, dest); + console.log(` Moved src/locales/${locale}/ → scripts/deleted/locales-${locale}/`); + } + + // Rename default locale folder + if (editOldDefaultToNewDefault) { + const src = join(localesDir, defaultLocale); + const dest = join(localesDir, newDefaultLocale); + if (existsSync(src)) { + await fs.rename(src, dest); + console.log(` Renamed src/locales/${defaultLocale}/ → src/locales/${newDefaultLocale}/`); + } + } } // ─── Phase F: src/pages/ ────────────────────────────────────────────────────── @@ -327,428 +369,506 @@ const ROOT_ONLY_PAGES = new Set(["admin.astro"]); // Returns the subfolder name where a locale's pages live, or null for root. function defaultPagesDir(prefix, locale) { - return prefix ? locale : null; + return prefix ? locale : null; } async function patchPagesFolders({ - defaultLocale, newDefaultLocale, currentLocales, - localesToAdd, localesToRemove, editOldDefaultToNewDefault, - prefixDefaultLocale: newPrefixDefaultLocale, - currentPrefixDefaultLocale, + defaultLocale, + newDefaultLocale, + currentLocales, + localesToAdd, + localesToRemove, + editOldDefaultToNewDefault, + prefixDefaultLocale: newPrefixDefaultLocale, + currentPrefixDefaultLocale, }) { - const pagesDir = join(root, "src", "pages"); - const deletedDir = join(root, "scripts", "deleted"); - const handledLocales = new Set(); - - // ── Step 1: Handle default locale pages structure ───────────────────────── - // null = pages live at src/pages/ root; string = pages live at src/pages/{locale}/ - const oldDefaultDir = defaultPagesDir(currentPrefixDefaultLocale, defaultLocale); - const newDefaultTargetDir = defaultPagesDir(newPrefixDefaultLocale, newDefaultLocale); - - if (defaultLocale === newDefaultLocale) { - // Locale unchanged — only prefix may have changed - if (oldDefaultDir !== newDefaultTargetDir) { - if (oldDefaultDir === null) { - // false → true: move root pages into {locale}/ - const rootEntries = await fs.readdir(pagesDir, { withFileTypes: true }); - const rootItems = rootEntries.filter((e) => !isLocaleDir(e.name) && !ROOT_ONLY_PAGES.has(e.name)); - const destDir = join(pagesDir, newDefaultTargetDir); - await fs.mkdir(destDir, { recursive: true }); - for (const e of rootItems) { - await fs.rename(join(pagesDir, e.name), join(destDir, e.name)); - } - console.log(` Moved root pages → src/pages/${newDefaultTargetDir}/ (prefixDefaultLocale: false → true)`); - } else { - // true → false: move {locale}/ contents to root - const srcDir = join(pagesDir, oldDefaultDir); - if (existsSync(srcDir)) { - const srcEntries = await fs.readdir(srcDir, { withFileTypes: true }); - for (const e of srcEntries) { - await fs.rename(join(srcDir, e.name), join(pagesDir, e.name)); - } - await fs.rm(srcDir, { recursive: true, force: true }); - console.log(` Moved src/pages/${oldDefaultDir}/ to root (prefixDefaultLocale: true → false)`); - } - } - handledLocales.add(defaultLocale); - } - // else: nothing to do for default locale - - } else if (editOldDefaultToNewDefault) { - // Old default locale is being renamed to new default — pages stay in place - if (oldDefaultDir === null) { - console.log(` ℹ️ Root pages now represent "${newDefaultLocale}" — update page content manually`); - } else { - // prefix=true: rename {defaultLocale}/ → {newDefaultLocale}/ - const srcDir = join(pagesDir, defaultLocale); - const destDir = join(pagesDir, newDefaultLocale); - if (existsSync(srcDir)) { - await fs.rename(srcDir, destDir); - console.log(` Renamed src/pages/${defaultLocale}/ → src/pages/${newDefaultLocale}/`); - } - } - handledLocales.add(defaultLocale); - handledLocales.add(newDefaultLocale); - - } else { - // Locale changed and new default was previously a non-default locale - // (its pages currently live in src/pages/{newDefaultLocale}/) - const newDefaultCurrentPath = join(pagesDir, newDefaultLocale); - - if (newDefaultTargetDir === null && oldDefaultDir === null) { - // false → false, locale change: swap subfolder ↔ root (needs temp dir) - if (existsSync(newDefaultCurrentPath)) { - const tempDir = join(pagesDir, SWAP_TMP); - - // 1a. Move new default subfolder → temp - await fs.rename(newDefaultCurrentPath, tempDir); - - // 1b. Move old default root pages → subfolder or deleted - const rootEntries = await fs.readdir(pagesDir, { withFileTypes: true }); - const rootItems = rootEntries.filter((e) => e.name !== SWAP_TMP && !isLocaleDir(e.name) && !ROOT_ONLY_PAGES.has(e.name)); - - if (localesToRemove.includes(defaultLocale)) { - await fs.mkdir(deletedDir, { recursive: true }); - const dest = join(deletedDir, `pages-${defaultLocale}`); - if (existsSync(dest)) await fs.rm(dest, { recursive: true }); - await fs.mkdir(dest); - for (const e of rootItems) { - await fs.rename(join(pagesDir, e.name), join(dest, e.name)); - } - console.log(` Moved root pages (${defaultLocale}) → scripts/deleted/pages-${defaultLocale}/`); - } else { - const oldDefaultPath = join(pagesDir, defaultLocale); - await fs.mkdir(oldDefaultPath, { recursive: true }); - for (const e of rootItems) { - await fs.rename(join(pagesDir, e.name), join(oldDefaultPath, e.name)); - } - console.log(` Created src/pages/${defaultLocale}/ (moved from root — was default locale)`); - } - handledLocales.add(defaultLocale); - - // 1c. Promote temp → root - const tempEntries = await fs.readdir(tempDir, { withFileTypes: true }); - for (const e of tempEntries) { - await fs.rename(join(tempDir, e.name), join(pagesDir, e.name)); - } - await fs.rm(tempDir, { recursive: true, force: true }); - console.log(` Promoted src/pages/${newDefaultLocale}/ to root (new default locale)`); - handledLocales.add(newDefaultLocale); - } - - } else if (newDefaultTargetDir === null && oldDefaultDir !== null) { - // true → false, locale change: promote {newDefaultLocale}/ to root; old default stays as subfolder - if (existsSync(newDefaultCurrentPath)) { - const srcEntries = await fs.readdir(newDefaultCurrentPath, { withFileTypes: true }); - for (const e of srcEntries) { - await fs.rename(join(newDefaultCurrentPath, e.name), join(pagesDir, e.name)); - } - await fs.rm(newDefaultCurrentPath, { recursive: true, force: true }); - console.log(` Promoted src/pages/${newDefaultLocale}/ to root (new default locale)`); - } - handledLocales.add(newDefaultLocale); - // oldDefaultDir ({defaultLocale}/) stays in place as a non-default subfolder - - } else if (oldDefaultDir === null && newDefaultTargetDir !== null) { - // false → true, locale change: move root → {defaultLocale}/; new default stays in {newDefaultLocale}/ - const rootEntries = await fs.readdir(pagesDir, { withFileTypes: true }); - const rootItems = rootEntries.filter((e) => !isLocaleDir(e.name) && !ROOT_ONLY_PAGES.has(e.name)); - - if (localesToRemove.includes(defaultLocale)) { - await fs.mkdir(deletedDir, { recursive: true }); - const dest = join(deletedDir, `pages-${defaultLocale}`); - if (existsSync(dest)) await fs.rm(dest, { recursive: true }); - await fs.mkdir(dest); - for (const e of rootItems) { - await fs.rename(join(pagesDir, e.name), join(dest, e.name)); - } - console.log(` Moved root pages (${defaultLocale}) → scripts/deleted/pages-${defaultLocale}/`); - } else { - const oldDefaultPath = join(pagesDir, defaultLocale); - await fs.mkdir(oldDefaultPath, { recursive: true }); - for (const e of rootItems) { - await fs.rename(join(pagesDir, e.name), join(oldDefaultPath, e.name)); - } - console.log(` Created src/pages/${defaultLocale}/ (moved from root — was default locale)`); - } - handledLocales.add(defaultLocale); - // newDefaultLocale already at {newDefaultLocale}/ (its final location), no move needed - handledLocales.add(newDefaultLocale); - - } - // else: true → true, locale change: no page folder moves required (config only) - } - - // ── Step 2: Add new non-default locale folders ──────────────────────────── - // Find a template locale (prefer a locale that was already non-default and stays non-default) - let templateLocale = null; - const templateCandidates = [...currentLocales, defaultLocale]; - for (const locale of templateCandidates) { - if (locale === newDefaultLocale) continue; - if (localesToRemove.includes(locale)) continue; - if (existsSync(join(pagesDir, locale))) { templateLocale = locale; break; } - } - // Fallback: allow copying from a locale being removed if it's the only option - // (must run before Step 3 removes it below) - if (!templateLocale) { - for (const locale of templateCandidates) { - if (locale === newDefaultLocale) continue; - if (existsSync(join(pagesDir, locale))) { templateLocale = locale; break; } - } - } - - for (const locale of localesToAdd) { - if (locale === newDefaultLocale) continue; - const dest = join(pagesDir, locale); - if (existsSync(dest)) { - console.log(` Skipped src/pages/${locale}/ — already exists`); - continue; - } - if (templateLocale) { - await fs.cp(join(pagesDir, templateLocale), dest, { recursive: true }); - for (const page of ROOT_ONLY_PAGES) { - const copied = join(dest, page); - if (existsSync(copied)) await fs.rm(copied); - } - console.log(` Created src/pages/${locale}/ (copied from src/pages/${templateLocale}/)`); - console.log(` ⚠️ Content in src/pages/${locale}/ is in ${templateLocale} — translate manually`); - } else { - console.log(` ⚠️ Could not scaffold src/pages/${locale}/ — no existing locale folder to copy from`); - } - } - - // ── Step 3: Remove non-default locale folders (skipping already-handled) ── - for (const locale of localesToRemove) { - if (handledLocales.has(locale)) continue; - const src = join(pagesDir, locale); - if (!existsSync(src)) continue; - await fs.mkdir(deletedDir, { recursive: true }); - const dest = join(deletedDir, `pages-${locale}`); - if (existsSync(dest)) await fs.rm(dest, { recursive: true }); - await fs.rename(src, dest); - console.log(` Moved src/pages/${locale}/ → scripts/deleted/pages-${locale}/`); - } + const pagesDir = join(root, "src", "pages"); + const deletedDir = join(root, "scripts", "deleted"); + const handledLocales = new Set(); + + // ── Step 1: Handle default locale pages structure ───────────────────────── + // null = pages live at src/pages/ root; string = pages live at src/pages/{locale}/ + const oldDefaultDir = defaultPagesDir(currentPrefixDefaultLocale, defaultLocale); + const newDefaultTargetDir = defaultPagesDir(newPrefixDefaultLocale, newDefaultLocale); + + if (defaultLocale === newDefaultLocale) { + // Locale unchanged — only prefix may have changed + if (oldDefaultDir !== newDefaultTargetDir) { + if (oldDefaultDir === null) { + // false → true: move root pages into {locale}/ + const rootEntries = await fs.readdir(pagesDir, { withFileTypes: true }); + const rootItems = rootEntries.filter( + (e) => !isLocaleDir(e.name) && !ROOT_ONLY_PAGES.has(e.name), + ); + const destDir = join(pagesDir, newDefaultTargetDir); + await fs.mkdir(destDir, { recursive: true }); + for (const e of rootItems) { + await fs.rename(join(pagesDir, e.name), join(destDir, e.name)); + } + console.log( + ` Moved root pages → src/pages/${newDefaultTargetDir}/ (prefixDefaultLocale: false → true)`, + ); + } else { + // true → false: move {locale}/ contents to root + const srcDir = join(pagesDir, oldDefaultDir); + if (existsSync(srcDir)) { + const srcEntries = await fs.readdir(srcDir, { withFileTypes: true }); + for (const e of srcEntries) { + await fs.rename(join(srcDir, e.name), join(pagesDir, e.name)); + } + await fs.rm(srcDir, { recursive: true, force: true }); + console.log( + ` Moved src/pages/${oldDefaultDir}/ to root (prefixDefaultLocale: true → false)`, + ); + } + } + handledLocales.add(defaultLocale); + } + // else: nothing to do for default locale + } else if (editOldDefaultToNewDefault) { + // Old default locale is being renamed to new default — pages stay in place + if (oldDefaultDir === null) { + console.log( + ` ℹ️ Root pages now represent "${newDefaultLocale}" — update page content manually`, + ); + } else { + // prefix=true: rename {defaultLocale}/ → {newDefaultLocale}/ + const srcDir = join(pagesDir, defaultLocale); + const destDir = join(pagesDir, newDefaultLocale); + if (existsSync(srcDir)) { + await fs.rename(srcDir, destDir); + console.log(` Renamed src/pages/${defaultLocale}/ → src/pages/${newDefaultLocale}/`); + } + } + handledLocales.add(defaultLocale); + handledLocales.add(newDefaultLocale); + } else { + // Locale changed and new default was previously a non-default locale + // (its pages currently live in src/pages/{newDefaultLocale}/) + const newDefaultCurrentPath = join(pagesDir, newDefaultLocale); + + if (newDefaultTargetDir === null && oldDefaultDir === null) { + // false → false, locale change: swap subfolder ↔ root (needs temp dir) + if (existsSync(newDefaultCurrentPath)) { + const tempDir = join(pagesDir, SWAP_TMP); + + // 1a. Move new default subfolder → temp + await fs.rename(newDefaultCurrentPath, tempDir); + + // 1b. Move old default root pages → subfolder or deleted + const rootEntries = await fs.readdir(pagesDir, { withFileTypes: true }); + const rootItems = rootEntries.filter( + (e) => e.name !== SWAP_TMP && !isLocaleDir(e.name) && !ROOT_ONLY_PAGES.has(e.name), + ); + + if (localesToRemove.includes(defaultLocale)) { + await fs.mkdir(deletedDir, { recursive: true }); + const dest = join(deletedDir, `pages-${defaultLocale}`); + if (existsSync(dest)) await fs.rm(dest, { recursive: true }); + await fs.mkdir(dest); + for (const e of rootItems) { + await fs.rename(join(pagesDir, e.name), join(dest, e.name)); + } + console.log( + ` Moved root pages (${defaultLocale}) → scripts/deleted/pages-${defaultLocale}/`, + ); + } else { + const oldDefaultPath = join(pagesDir, defaultLocale); + await fs.mkdir(oldDefaultPath, { recursive: true }); + for (const e of rootItems) { + await fs.rename(join(pagesDir, e.name), join(oldDefaultPath, e.name)); + } + console.log( + ` Created src/pages/${defaultLocale}/ (moved from root — was default locale)`, + ); + } + handledLocales.add(defaultLocale); + + // 1c. Promote temp → root + const tempEntries = await fs.readdir(tempDir, { withFileTypes: true }); + for (const e of tempEntries) { + await fs.rename(join(tempDir, e.name), join(pagesDir, e.name)); + } + await fs.rm(tempDir, { recursive: true, force: true }); + console.log(` Promoted src/pages/${newDefaultLocale}/ to root (new default locale)`); + handledLocales.add(newDefaultLocale); + } + } else if (newDefaultTargetDir === null && oldDefaultDir !== null) { + // true → false, locale change: promote {newDefaultLocale}/ to root; old default stays as subfolder + if (existsSync(newDefaultCurrentPath)) { + const srcEntries = await fs.readdir(newDefaultCurrentPath, { withFileTypes: true }); + for (const e of srcEntries) { + await fs.rename(join(newDefaultCurrentPath, e.name), join(pagesDir, e.name)); + } + await fs.rm(newDefaultCurrentPath, { recursive: true, force: true }); + console.log(` Promoted src/pages/${newDefaultLocale}/ to root (new default locale)`); + } + handledLocales.add(newDefaultLocale); + // oldDefaultDir ({defaultLocale}/) stays in place as a non-default subfolder + } else if (oldDefaultDir === null && newDefaultTargetDir !== null) { + // false → true, locale change: move root → {defaultLocale}/; new default stays in {newDefaultLocale}/ + const rootEntries = await fs.readdir(pagesDir, { withFileTypes: true }); + const rootItems = rootEntries.filter( + (e) => !isLocaleDir(e.name) && !ROOT_ONLY_PAGES.has(e.name), + ); + + if (localesToRemove.includes(defaultLocale)) { + await fs.mkdir(deletedDir, { recursive: true }); + const dest = join(deletedDir, `pages-${defaultLocale}`); + if (existsSync(dest)) await fs.rm(dest, { recursive: true }); + await fs.mkdir(dest); + for (const e of rootItems) { + await fs.rename(join(pagesDir, e.name), join(dest, e.name)); + } + console.log( + ` Moved root pages (${defaultLocale}) → scripts/deleted/pages-${defaultLocale}/`, + ); + } else { + const oldDefaultPath = join(pagesDir, defaultLocale); + await fs.mkdir(oldDefaultPath, { recursive: true }); + for (const e of rootItems) { + await fs.rename(join(pagesDir, e.name), join(oldDefaultPath, e.name)); + } + console.log(` Created src/pages/${defaultLocale}/ (moved from root — was default locale)`); + } + handledLocales.add(defaultLocale); + // newDefaultLocale already at {newDefaultLocale}/ (its final location), no move needed + handledLocales.add(newDefaultLocale); + } + // else: true → true, locale change: no page folder moves required (config only) + } + + // ── Step 2: Add new non-default locale folders ──────────────────────────── + // Find a template locale (prefer a locale that was already non-default and stays non-default) + let templateLocale = null; + const templateCandidates = [...currentLocales, defaultLocale]; + for (const locale of templateCandidates) { + if (locale === newDefaultLocale) continue; + if (localesToRemove.includes(locale)) continue; + if (existsSync(join(pagesDir, locale))) { + templateLocale = locale; + break; + } + } + // Fallback: allow copying from a locale being removed if it's the only option + // (must run before Step 3 removes it below) + if (!templateLocale) { + for (const locale of templateCandidates) { + if (locale === newDefaultLocale) continue; + if (existsSync(join(pagesDir, locale))) { + templateLocale = locale; + break; + } + } + } + + for (const locale of localesToAdd) { + if (locale === newDefaultLocale) continue; + const dest = join(pagesDir, locale); + if (existsSync(dest)) { + console.log(` Skipped src/pages/${locale}/ — already exists`); + continue; + } + if (templateLocale) { + await fs.cp(join(pagesDir, templateLocale), dest, { recursive: true }); + for (const page of ROOT_ONLY_PAGES) { + const copied = join(dest, page); + if (existsSync(copied)) await fs.rm(copied); + } + console.log(` Created src/pages/${locale}/ (copied from src/pages/${templateLocale}/)`); + console.log( + ` ⚠️ Content in src/pages/${locale}/ is in ${templateLocale} — translate manually`, + ); + } else { + console.log( + ` ⚠️ Could not scaffold src/pages/${locale}/ — no existing locale folder to copy from`, + ); + } + } + + // ── Step 3: Remove non-default locale folders (skipping already-handled) ── + for (const locale of localesToRemove) { + if (handledLocales.has(locale)) continue; + const src = join(pagesDir, locale); + if (!existsSync(src)) continue; + await fs.mkdir(deletedDir, { recursive: true }); + const dest = join(deletedDir, `pages-${locale}`); + if (existsSync(dest)) await fs.rm(dest, { recursive: true }); + await fs.rename(src, dest); + console.log(` Moved src/pages/${locale}/ → scripts/deleted/pages-${locale}/`); + } } // ─── Phase G: src/content/ ──────────────────────────────────────────────────── -async function patchContentFolders({ defaultLocale, newDefaultLocale, localesToAdd, localesToRemove, editOldDefaultToNewDefault }) { - const contentDir = join(root, "src", "content"); - if (!existsSync(contentDir)) return; - - const deletedDir = join(root, "scripts", "deleted"); - let collections; - try { - collections = await fs.readdir(contentDir, { withFileTypes: true }); - } catch { return; } - - for (const entry of collections) { - if (!entry.isDirectory()) continue; - const collectionDir = join(contentDir, entry.name); - - // Only touch collections that have locale subdirectories - let subDirs; - try { - subDirs = await fs.readdir(collectionDir, { withFileTypes: true }); - } catch { continue; } - - const hasLocaleDirs = subDirs.some( - (d) => d.isDirectory() && /^[a-z]{2}(-[a-z]{2})?$/i.test(d.name), - ); - if (!hasLocaleDirs) continue; - - // Add: create empty locale folder (content is user's responsibility) - for (const locale of localesToAdd) { - const dest = join(collectionDir, locale); - if (!existsSync(dest)) { - await fs.mkdir(dest, { recursive: true }); - console.log(` Created src/content/${entry.name}/${locale}/ (empty)`); - } - } - - // Remove → move to scripts/deleted/ - for (const locale of localesToRemove) { - const src = join(collectionDir, locale); - if (!existsSync(src)) continue; - await fs.mkdir(deletedDir, { recursive: true }); - const dest = join(deletedDir, `content-${entry.name}-${locale}`); - if (existsSync(dest)) await fs.rm(dest, { recursive: true }); - await fs.rename(src, dest); - console.log(` Moved src/content/${entry.name}/${locale}/ → scripts/deleted/content-${entry.name}-${locale}/`); - } - - // Rename default locale folder - if (editOldDefaultToNewDefault) { - const src = join(collectionDir, defaultLocale); - const dest = join(collectionDir, newDefaultLocale); - if (existsSync(src)) { - await fs.rename(src, dest); - console.log(` Renamed src/content/${entry.name}/${defaultLocale}/ → src/content/${entry.name}/${newDefaultLocale}/`); - } - } - } +async function patchContentFolders({ + defaultLocale, + newDefaultLocale, + localesToAdd, + localesToRemove, + editOldDefaultToNewDefault, +}) { + const contentDir = join(root, "src", "content"); + if (!existsSync(contentDir)) return; + + const deletedDir = join(root, "scripts", "deleted"); + let collections; + try { + collections = await fs.readdir(contentDir, { withFileTypes: true }); + } catch { + return; + } + + for (const entry of collections) { + if (!entry.isDirectory()) continue; + const collectionDir = join(contentDir, entry.name); + + // Only touch collections that have locale subdirectories + let subDirs; + try { + subDirs = await fs.readdir(collectionDir, { withFileTypes: true }); + } catch { + continue; + } + + const hasLocaleDirs = subDirs.some( + (d) => d.isDirectory() && /^[a-z]{2}(-[a-z]{2})?$/i.test(d.name), + ); + if (!hasLocaleDirs) continue; + + // Add: create empty locale folder (content is user's responsibility) + for (const locale of localesToAdd) { + const dest = join(collectionDir, locale); + if (!existsSync(dest)) { + await fs.mkdir(dest, { recursive: true }); + console.log(` Created src/content/${entry.name}/${locale}/ (empty)`); + } + } + + // Remove → move to scripts/deleted/ + for (const locale of localesToRemove) { + const src = join(collectionDir, locale); + if (!existsSync(src)) continue; + await fs.mkdir(deletedDir, { recursive: true }); + const dest = join(deletedDir, `content-${entry.name}-${locale}`); + if (existsSync(dest)) await fs.rm(dest, { recursive: true }); + await fs.rename(src, dest); + console.log( + ` Moved src/content/${entry.name}/${locale}/ → scripts/deleted/content-${entry.name}-${locale}/`, + ); + } + + // Rename default locale folder + if (editOldDefaultToNewDefault) { + const src = join(collectionDir, defaultLocale); + const dest = join(collectionDir, newDefaultLocale); + if (existsSync(src)) { + await fs.rename(src, dest); + console.log( + ` Renamed src/content/${entry.name}/${defaultLocale}/ → src/content/${entry.name}/${newDefaultLocale}/`, + ); + } + } + } } // ─── Main ───────────────────────────────────────────────────────────────────── async function configI18n() { - // ── Readline setup (queue-based to avoid stdin race with async/await) ────── - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - const lineQueue = []; - const waiters = []; - rl.on("line", (line) => { - if (waiters.length > 0) waiters.shift()(line); - else lineQueue.push(line); - }); - const ask = (q) => { - process.stdout.write(q); - if (lineQueue.length > 0) return Promise.resolve(lineQueue.shift()); - return new Promise((resolve) => waiters.push(resolve)); - }; - - // ── Read current config ─────────────────────────────────────────────────── - const current = readI18nConfig(root); - if (!current) { - console.error("Could not read i18n config from src/features/i18n/i18nConfig.ts. Exiting."); - rl.close(); - process.exit(1); - } - const { defaultLocale, locales: currentLocales } = current; - - // Read current prefixDefaultLocale from astro.config.ts - let currentPrefixDefaultLocale = false; - try { - const astroConfig = await fs.readFile(join(root, "astro.config.ts"), "utf-8"); - const m = astroConfig.match(/prefixDefaultLocale:\s*(true|false)/); - currentPrefixDefaultLocale = m?.[1] === "true"; - } catch { /* keep false */ } - - console.log(`\nCurrent config: defaultLocale="${defaultLocale}", locales=[${currentLocales.join(", ")}], prefixDefaultLocale=${currentPrefixDefaultLocale}\n`); - console.log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); - console.log(" Current i18n configuration"); - console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); - console.log(`Default locale: ${defaultLocale}`); - console.log(`Locales: ${currentLocales.join(", ")}`); - console.log(`Prefix default locale: ${currentPrefixDefaultLocale ? "yes" : "no"}`); - console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); - - console.log("Locale examples:"); - console.log("https://github.com/cospired/i18n-iso-languages\n"); - - // ── Prompt 1: new default locale ───────────────────────────────────────── - let newDefaultLocale; - while (true) { - const answer = (await ask(`\nDefault locale? [${defaultLocale}]: `)).trim(); - const val = answer === "" ? defaultLocale : answer.toLowerCase(); - if (validateLocale(val)) { newDefaultLocale = val; break; } - console.log(' Invalid locale. Use a 2-letter code like "en", "fr", or "de".'); - } - - // ── Prompt 3: additional locales ────────────────────────────────────────── - let additionalLocales; - while (true) { - const answer = (await ask("Additional locales (comma-separated, e.g. fr, de): ")).trim(); - if (!answer) { console.log(" Please enter at least one additional locale."); continue; } - const parsed = answer.split(",").map((l) => l.trim().toLowerCase()).filter(Boolean); - const invalid = parsed.filter((l) => !validateLocale(l)); - if (invalid.length > 0) { console.log(` Invalid: ${invalid.join(", ")}. Use 2-letter codes.`); continue; } - additionalLocales = parsed; - break; - } - - // Build final locales list (default first, deduplicated) - const newLocales = [...new Set([newDefaultLocale, ...additionalLocales])]; - - // ── Prompt 4: prefixDefaultLocale ───────────────────────────────────────── - const prefixDefaultLocale = await askYesNo( - ask, - "\nPrefix default locale in URLs? (/en/about vs /about)", - currentPrefixDefaultLocale, - ); - - // ── Confirm ─────────────────────────────────────────────────────────────── - console.log(`\nNew config:`); - console.log(` defaultLocale: "${newDefaultLocale}"`); - console.log(` locales: [${newLocales.join(", ")}]`); - console.log(` prefixDefaultLocale: ${prefixDefaultLocale}`); - - const confirm = await askYesNo(ask, "\nProceed?", false); - rl.close(); - - if (!confirm) { - console.log("Aborted. No files were changed."); - return; - } - - console.log(); - - // ── Determine operations ────────────────────────────────────────────────── - const { localesToAdd, localesToRemove, editOldDefaultToNewDefault } = determineOperations({ - defaultLocale, - currentLocales, - newDefaultLocale, - newLocales, - }); - - if (localesToAdd.length === 0 && localesToRemove.length === 0 && defaultLocale === newDefaultLocale && prefixDefaultLocale === currentPrefixDefaultLocale) { - console.log("No changes needed — config already matches.\n"); - return; - } - - if (localesToAdd.length > 0) console.log(`Adding: ${localesToAdd.join(", ")}`); - if (localesToRemove.length > 0) console.log(`Removing: ${localesToRemove.join(", ")}`); - if (editOldDefaultToNewDefault) console.log(`Renaming default: ${defaultLocale} → ${newDefaultLocale}`); - if (prefixDefaultLocale !== currentPrefixDefaultLocale) console.log(`prefixDefaultLocale: ${currentPrefixDefaultLocale} → ${prefixDefaultLocale}`); - console.log(); - - const ops = { defaultLocale, newDefaultLocale, currentLocales, newLocales, localesToAdd, localesToRemove, editOldDefaultToNewDefault, prefixDefaultLocale, currentPrefixDefaultLocale }; - - // ── Phase A ─────────────────────────────────────────────────────────────── - console.log("Phase A: astro.config.ts..."); - await patchAstroConfig(ops); - - // ── Phase B ─────────────────────────────────────────────────────────────── - console.log("\nPhase B: siteSettings.ts..."); - await patchSiteSettings(ops); - - // ── Phase C ─────────────────────────────────────────────────────────────── - console.log("\nPhase C: routeTranslations.ts..."); - await patchRouteTranslations(ops); - - // ── Phase D ─────────────────────────────────────────────────────────────── - console.log("\nPhase D: Decap config..."); - await patchDecapConfig(ops); - - // ── Phase E ─────────────────────────────────────────────────────────────── - console.log("\nPhase E: src/locales/..."); - await patchLocalesFolders(ops); - - // ── Phase F ─────────────────────────────────────────────────────────────── - console.log("\nPhase F: src/pages/..."); - await patchPagesFolders(ops); - - // ── Phase G ─────────────────────────────────────────────────────────────── - console.log("\nPhase G: src/content/..."); - await patchContentFolders(ops); - - // ── Summary ─────────────────────────────────────────────────────────────── - console.log("\n...done!\n"); - console.log("====================================="); - console.log(" i18n configuration updated"); - console.log("=====================================\n"); - - console.log("Next steps:"); - let step = 1; - if (localesToAdd.length > 0) { - console.log(`${step++}. Translate strings in src/locales/${localesToAdd.join("/ and src/locales/")}/`); - console.log(`${step++}. Add translated URL slugs for each new locale in src/data/navData.json`); - console.log(`${step++}. Review auto-generated localeMap values in src/features/i18n/i18nConfig.ts`); - } - console.log(`${step++}. Run \`npm run dev\` to verify the site loads`); - console.log(); + // ── Readline setup (queue-based to avoid stdin race with async/await) ────── + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const lineQueue = []; + const waiters = []; + rl.on("line", (line) => { + if (waiters.length > 0) waiters.shift()(line); + else lineQueue.push(line); + }); + const ask = (q) => { + process.stdout.write(q); + if (lineQueue.length > 0) return Promise.resolve(lineQueue.shift()); + return new Promise((resolve) => waiters.push(resolve)); + }; + + // ── Read current config ─────────────────────────────────────────────────── + const current = readI18nConfig(root); + if (!current) { + console.error("Could not read i18n config from src/features/i18n/i18nConfig.ts. Exiting."); + rl.close(); + process.exit(1); + } + const { defaultLocale, locales: currentLocales } = current; + + // Read current prefixDefaultLocale from astro.config.ts + let currentPrefixDefaultLocale = false; + try { + const astroConfig = await fs.readFile(join(root, "astro.config.ts"), "utf-8"); + const m = astroConfig.match(/prefixDefaultLocale:\s*(true|false)/); + currentPrefixDefaultLocale = m?.[1] === "true"; + } catch { + /* keep false */ + } + + console.log( + `\nCurrent config: defaultLocale="${defaultLocale}", locales=[${currentLocales.join(", ")}], prefixDefaultLocale=${currentPrefixDefaultLocale}\n`, + ); + console.log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + console.log(" Current i18n configuration"); + console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + console.log(`Default locale: ${defaultLocale}`); + console.log(`Locales: ${currentLocales.join(", ")}`); + console.log(`Prefix default locale: ${currentPrefixDefaultLocale ? "yes" : "no"}`); + console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); + + console.log("Locale examples:"); + console.log("https://github.com/cospired/i18n-iso-languages\n"); + + // ── Prompt 1: new default locale ───────────────────────────────────────── + let newDefaultLocale; + while (true) { + const answer = (await ask(`\nDefault locale? [${defaultLocale}]: `)).trim(); + const val = answer === "" ? defaultLocale : answer.toLowerCase(); + if (validateLocale(val)) { + newDefaultLocale = val; + break; + } + console.log(' Invalid locale. Use a 2-letter code like "en", "fr", or "de".'); + } + + // ── Prompt 3: additional locales ────────────────────────────────────────── + let additionalLocales; + while (true) { + const answer = (await ask("Additional locales (comma-separated, e.g. fr, de): ")).trim(); + if (!answer) { + console.log(" Please enter at least one additional locale."); + continue; + } + const parsed = answer + .split(",") + .map((l) => l.trim().toLowerCase()) + .filter(Boolean); + const invalid = parsed.filter((l) => !validateLocale(l)); + if (invalid.length > 0) { + console.log(` Invalid: ${invalid.join(", ")}. Use 2-letter codes.`); + continue; + } + additionalLocales = parsed; + break; + } + + // Build final locales list (default first, deduplicated) + const newLocales = [...new Set([newDefaultLocale, ...additionalLocales])]; + + // ── Prompt 4: prefixDefaultLocale ───────────────────────────────────────── + const prefixDefaultLocale = await askYesNo( + ask, + "\nPrefix default locale in URLs? (/en/about vs /about)", + currentPrefixDefaultLocale, + ); + + // ── Confirm ─────────────────────────────────────────────────────────────── + console.log(`\nNew config:`); + console.log(` defaultLocale: "${newDefaultLocale}"`); + console.log(` locales: [${newLocales.join(", ")}]`); + console.log(` prefixDefaultLocale: ${prefixDefaultLocale}`); + + const confirm = await askYesNo(ask, "\nProceed?", false); + rl.close(); + + if (!confirm) { + console.log("Aborted. No files were changed."); + return; + } + + console.log(); + + // ── Determine operations ────────────────────────────────────────────────── + const { localesToAdd, localesToRemove, editOldDefaultToNewDefault } = determineOperations({ + defaultLocale, + currentLocales, + newDefaultLocale, + newLocales, + }); + + if ( + localesToAdd.length === 0 && + localesToRemove.length === 0 && + defaultLocale === newDefaultLocale && + prefixDefaultLocale === currentPrefixDefaultLocale + ) { + console.log("No changes needed — config already matches.\n"); + return; + } + + if (localesToAdd.length > 0) console.log(`Adding: ${localesToAdd.join(", ")}`); + if (localesToRemove.length > 0) console.log(`Removing: ${localesToRemove.join(", ")}`); + if (editOldDefaultToNewDefault) + console.log(`Renaming default: ${defaultLocale} → ${newDefaultLocale}`); + if (prefixDefaultLocale !== currentPrefixDefaultLocale) + console.log(`prefixDefaultLocale: ${currentPrefixDefaultLocale} → ${prefixDefaultLocale}`); + console.log(); + + const ops = { + defaultLocale, + newDefaultLocale, + currentLocales, + newLocales, + localesToAdd, + localesToRemove, + editOldDefaultToNewDefault, + prefixDefaultLocale, + currentPrefixDefaultLocale, + }; + + // ── Phase A ─────────────────────────────────────────────────────────────── + console.log("Phase A: astro.config.ts..."); + await patchAstroConfig(ops); + + // ── Phase B ─────────────────────────────────────────────────────────────── + console.log("\nPhase B: siteSettings.ts..."); + await patchSiteSettings(ops); + + // ── Phase C ─────────────────────────────────────────────────────────────── + console.log("\nPhase C: routeTranslations.ts..."); + await patchRouteTranslations(ops); + + // ── Phase D ─────────────────────────────────────────────────────────────── + console.log("\nPhase D: Decap config..."); + await patchDecapConfig(ops); + + // ── Phase E ─────────────────────────────────────────────────────────────── + console.log("\nPhase E: src/locales/..."); + await patchLocalesFolders(ops); + + // ── Phase F ─────────────────────────────────────────────────────────────── + console.log("\nPhase F: src/pages/..."); + await patchPagesFolders(ops); + + // ── Phase G ─────────────────────────────────────────────────────────────── + console.log("\nPhase G: src/content/..."); + await patchContentFolders(ops); + + // ── Summary ─────────────────────────────────────────────────────────────── + console.log("\n...done!\n"); + console.log("====================================="); + console.log(" i18n configuration updated"); + console.log("=====================================\n"); + + console.log("Next steps:"); + let step = 1; + if (localesToAdd.length > 0) { + console.log( + `${step++}. Translate strings in src/locales/${localesToAdd.join("/ and src/locales/")}/`, + ); + console.log(`${step++}. Add translated URL slugs for each new locale in src/data/navData.json`); + console.log( + `${step++}. Review auto-generated localeMap values in src/features/i18n/i18nConfig.ts`, + ); + } + console.log(`${step++}. Run \`npm run dev\` to verify the site loads`); + console.log(); } configI18n().catch((err) => { - console.error("Fatal error:", err); - process.exit(1); + console.error("Fatal error:", err); + process.exit(1); }); diff --git a/scripts/create-page.js b/scripts/create-page.js index 6b7b403..984692b 100644 --- a/scripts/create-page.js +++ b/scripts/create-page.js @@ -9,290 +9,256 @@ import { checkFeatureFlagBeforeRun } from "./utils/feature-flags.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = process.env.SCRIPT_ROOT ?? join(__dirname, ".."); - -const i18nDisabled = checkFeatureFlagBeforeRun( - root, - "i18n", - "i18n" -); +const i18nDisabled = checkFeatureFlagBeforeRun(root, "i18n", "i18n"); const i18nEnabled = !i18nDisabled; // ─── Client data ────────────────────────────────────────────────────────────── function readClientData() { - const clientPath = join(root, "src", "data", "client.ts"); - const siteConfigPath = join(root, "src", "data", "siteConfig.ts"); - const businessName = existsSync(clientPath) - ? readFileSync(clientPath, "utf8").match(/BUSINESS\s*=\s*\{[\s\S]*?name:\s*["']([^"']+)["']/)?.[1] ?? null - : null; - const siteTitle = existsSync(siteConfigPath) - ? readFileSync(siteConfigPath, "utf8").match(/SITE\s*=\s*\{[\s\S]*?title:\s*["']([^"']+)["']/)?.[1] ?? null - : null; - if (!businessName && !siteTitle) return null; - return { - businessName, - siteTitle, - }; + const clientPath = join(root, "src", "data", "client.ts"); + const siteConfigPath = join(root, "src", "data", "siteConfig.ts"); + const businessName = existsSync(clientPath) + ? (readFileSync(clientPath, "utf8").match( + /BUSINESS\s*=\s*\{[\s\S]*?name:\s*["']([^"']+)["']/, + )?.[1] ?? null) + : null; + const siteTitle = existsSync(siteConfigPath) + ? (readFileSync(siteConfigPath, "utf8").match( + /SITE\s*=\s*\{[\s\S]*?title:\s*["']([^"']+)["']/, + )?.[1] ?? null) + : null; + if (!businessName && !siteTitle) return null; + return { + businessName, + siteTitle, + }; } - function registerNavItem(slugMap, labels) { - const navPath = join( - root, - "src", - "data", - "navData.json" - ); - - if (!existsSync(navPath)) { - return "missing"; - } - - const nav = JSON.parse( - readFileSync(navPath, "utf8") - ); - - if (nav.some(item => item.key === slugMap.en)) { - return "skipped"; - } - - nav.push({ - key: slugMap.en, - urls: Object.fromEntries( - Object.entries(slugMap) - .map(([locale, slug]) => [ - locale, - `/${slug}` - ]) - ), - label: labels, - children: [] - }); - - - writeFileSync( - navPath, - JSON.stringify(nav, null, "\t") + "\n", - "utf8" - ); - - - return "registered"; + const navPath = join(root, "src", "data", "navData.json"); + + if (!existsSync(navPath)) { + return "missing"; + } + + const nav = JSON.parse(readFileSync(navPath, "utf8")); + + if (nav.some((item) => item.key === slugMap.en)) { + return "skipped"; + } + + nav.push({ + key: slugMap.en, + urls: Object.fromEntries(Object.entries(slugMap).map(([locale, slug]) => [locale, `/${slug}`])), + label: labels, + children: [], + }); + + writeFileSync(navPath, JSON.stringify(nav, null, "\t") + "\n", "utf8"); + + return "registered"; } // ─── Locale detection ───────────────────────────────────────────────────────── function detectSecondaryLocales(defaultLocale) { - const pagesDir = join(root, "src", "pages"); - - if (!existsSync(pagesDir)) return []; - - return readdirSync(pagesDir, { withFileTypes: true }) - .filter( - (e) => - e.isDirectory() && - /^[a-z]{2}(-[a-z]{2})?$/i.test(e.name) && - e.name !== defaultLocale - ) - .map((e) => e.name); + const pagesDir = join(root, "src", "pages"); + + if (!existsSync(pagesDir)) return []; + + return readdirSync(pagesDir, { withFileTypes: true }) + .filter( + (e) => e.isDirectory() && /^[a-z]{2}(-[a-z]{2})?$/i.test(e.name) && e.name !== defaultLocale, + ) + .map((e) => e.name); } // ─── Route translations ──────────────────────────────────────────────────────── function registerInRouteTranslations(defaultSlug, slugMap) { - const rtPath = join( - root, - "src", - "features", - "i18n", - "routeTranslations.ts" - ); - if (!existsSync(rtPath)) return "missing"; - - let content = readFileSync(rtPath, "utf8"); - let anyInserted = false; - - for (const [locale, localeSlug] of Object.entries(slugMap)) { - const updated = insertIntoLocaleBlock(content, locale, defaultSlug, localeSlug); - if (updated !== null) { - content = updated; - anyInserted = true; - } - } - - if (!anyInserted) return "skipped"; - writeFileSync(rtPath, content, "utf8"); - return "registered"; + const rtPath = join(root, "src", "features", "i18n", "routeTranslations.ts"); + if (!existsSync(rtPath)) return "missing"; + + let content = readFileSync(rtPath, "utf8"); + let anyInserted = false; + + for (const [locale, localeSlug] of Object.entries(slugMap)) { + const updated = insertIntoLocaleBlock(content, locale, defaultSlug, localeSlug); + if (updated !== null) { + content = updated; + anyInserted = true; + } + } + + if (!anyInserted) return "skipped"; + writeFileSync(rtPath, content, "utf8"); + return "registered"; } // ─── Template helpers ───────────────────────────────────────────────────────── function applyTemplate(template, title) { - // Handles both the standard placeholder and the legacy French one - return template.replaceAll("Page Title", title).replaceAll("Titre de la page", title); + // Handles both the standard placeholder and the legacy French one + return template.replaceAll("Page Title", title).replaceAll("Titre de la page", title); } // ─── Main ───────────────────────────────────────────────────────────────────── async function main() { - const client = readClientData(); - if (client?.businessName) console.log(`Client: ${client.businessName}`); - - const input = process.argv[2]; - const secondaryInput = process.argv[3]; // optional fast-path for first secondary locale - - if (!input) { - console.log( - 'Please provide page names. Example: npm run create-page -- "Contact, About, Services"', - ); - process.exit(1); - } - - // ── Locale config ───────────────────────────────────────────────────────── - const i18nConfig = i18nEnabled - ? readI18nConfig(root) - : null; - const defaultLocale = - i18nConfig?.defaultLocale ?? "en"; - const secondaryLocales = i18nEnabled - ? detectSecondaryLocales(defaultLocale) - : []; - - // ── Templates ───────────────────────────────────────────────────────────── - const defaultTemplatePath = join(root, "src", "pages", "_template.astro"); - if (!existsSync(defaultTemplatePath)) { - console.log(`Template not found: src/pages/_template.astro`); - process.exit(1); - } - const defaultTemplate = readFileSync(defaultTemplatePath, "utf8"); - - const secondaryTemplates = {}; - for (const locale of secondaryLocales) { - secondaryTemplates[locale] = readFileSync( - join(root, "src", "pages", locale, "_template.astro"), - "utf8", - ); - } - - // ── Parse inputs ────────────────────────────────────────────────────────── - const pages = input.split(",").map((p) => p.trim()).filter(Boolean); - - // Fast-path: secondary locale names supplied via argv[3] (one locale, backward-compat) - const fastPathNames = secondaryInput - ? secondaryInput.split(",").map((p) => p.trim()).filter(Boolean) - : null; - - // ── Readline (only opened when interactive prompts are needed) ──────────── - const needsPrompts = secondaryLocales.length > 0 && !fastPathNames && process.stdin.isTTY; - let ask = null; - let rl = null; - - if (needsPrompts) { - rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - const lineQueue = []; - const waiters = []; - rl.on("line", (line) => { - if (waiters.length > 0) waiters.shift()(line); - else lineQueue.push(line); - }); - ask = (q) => { - process.stdout.write(q); - if (lineQueue.length > 0) return Promise.resolve(lineQueue.shift()); - return new Promise((resolve) => waiters.push(resolve)); - }; - } - - // ── Process pages ───────────────────────────────────────────────────────── - for (let idx = 0; idx < pages.length; idx++) { - const page = pages[idx]; - const defaultSlug = slugify(page); - if (!defaultSlug) continue; - - const defaultTitle = titleCase(page); - - // Build slug/title map for all locales - const slugMap = { [defaultLocale]: defaultSlug }; - const titleMap = { [defaultLocale]: defaultTitle }; - - if (secondaryLocales.length > 0) { - if (fastPathNames) { - // argv[3] applies to the first secondary locale; rest default to same slug - const [firstLocale, ...restLocales] = secondaryLocales; - const secName = fastPathNames[idx] ?? page; - slugMap[firstLocale] = slugify(secName) || defaultSlug; - titleMap[firstLocale] = titleCase(secName); - for (const locale of restLocales) { - slugMap[locale] = defaultSlug; - titleMap[locale] = defaultTitle; - } - } else if (ask) { - // Interactive: prompt for each secondary locale - console.log(`\nPage: "${page}" (${defaultLocale} slug: "${defaultSlug}")`); - for (const locale of secondaryLocales) { - const answer = (await ask(` ${locale} name [${page}]: `)).trim(); - const secName = answer || page; - slugMap[locale] = slugify(secName) || defaultSlug; - titleMap[locale] = titleCase(secName); - } - } else { - // Non-TTY with no argv[3]: use default for all secondary locales - for (const locale of secondaryLocales) { - slugMap[locale] = defaultSlug; - titleMap[locale] = defaultTitle; - } - } - } - - // Default locale page - const defaultPagePath = join(root, "src", "pages", `${defaultSlug}.astro`); - if (existsSync(defaultPagePath)) { - console.log(`Skipped src/pages/${defaultSlug}.astro — already exists`); - } else { - writeFileSync(defaultPagePath, applyTemplate(defaultTemplate, defaultTitle), "utf8"); - console.log(`Created src/pages/${defaultSlug}.astro`); - } - - // Secondary locale pages - for (const locale of secondaryLocales) { - const localeSlug = slugMap[locale]; - const localeTitle = titleMap[locale]; - const localePath = join(root, "src", "pages", locale, `${localeSlug}.astro`); - if (existsSync(localePath)) { - console.log(`Skipped src/pages/${locale}/${localeSlug}.astro — already exists`); - } else { - writeFileSync(localePath, applyTemplate(secondaryTemplates[locale], localeTitle), "utf8"); - console.log(`Created src/pages/${locale}/${localeSlug}.astro`); - } - } - - // ── navData.json ─────────────────────────────────────── - const navStatus = registerNavItem(slugMap, titleMap); - - if (navStatus === "registered") { - console.log("Registered in navData.json"); - } - // ── routeTranslations.ts ─────────────────────────────────────────────────── - if (i18nEnabled) { - const rtStatus = registerInRouteTranslations( - defaultSlug, - slugMap - ); - - if (rtStatus === "registered") { - console.log("Registered in routeTranslations.ts"); - } else if (rtStatus === "skipped") { - console.log( - `Skipped routeTranslations.ts — "${defaultSlug}" already registered` - ); - } - } - } - - if (rl) rl.close(); + const client = readClientData(); + if (client?.businessName) console.log(`Client: ${client.businessName}`); + + const input = process.argv[2]; + const secondaryInput = process.argv[3]; // optional fast-path for first secondary locale + + if (!input) { + console.log( + 'Please provide page names. Example: npm run create-page -- "Contact, About, Services"', + ); + process.exit(1); + } + + // ── Locale config ───────────────────────────────────────────────────────── + const i18nConfig = i18nEnabled ? readI18nConfig(root) : null; + const defaultLocale = i18nConfig?.defaultLocale ?? "en"; + const secondaryLocales = i18nEnabled ? detectSecondaryLocales(defaultLocale) : []; + + // ── Templates ───────────────────────────────────────────────────────────── + const defaultTemplatePath = join(root, "src", "pages", "_template.astro"); + if (!existsSync(defaultTemplatePath)) { + console.log(`Template not found: src/pages/_template.astro`); + process.exit(1); + } + const defaultTemplate = readFileSync(defaultTemplatePath, "utf8"); + + const secondaryTemplates = {}; + for (const locale of secondaryLocales) { + secondaryTemplates[locale] = readFileSync( + join(root, "src", "pages", locale, "_template.astro"), + "utf8", + ); + } + + // ── Parse inputs ────────────────────────────────────────────────────────── + const pages = input + .split(",") + .map((p) => p.trim()) + .filter(Boolean); + + // Fast-path: secondary locale names supplied via argv[3] (one locale, backward-compat) + const fastPathNames = secondaryInput + ? secondaryInput + .split(",") + .map((p) => p.trim()) + .filter(Boolean) + : null; + + // ── Readline (only opened when interactive prompts are needed) ──────────── + const needsPrompts = secondaryLocales.length > 0 && !fastPathNames && process.stdin.isTTY; + let ask = null; + let rl = null; + + if (needsPrompts) { + rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const lineQueue = []; + const waiters = []; + rl.on("line", (line) => { + if (waiters.length > 0) waiters.shift()(line); + else lineQueue.push(line); + }); + ask = (q) => { + process.stdout.write(q); + if (lineQueue.length > 0) return Promise.resolve(lineQueue.shift()); + return new Promise((resolve) => waiters.push(resolve)); + }; + } + + // ── Process pages ───────────────────────────────────────────────────────── + for (let idx = 0; idx < pages.length; idx++) { + const page = pages[idx]; + const defaultSlug = slugify(page); + if (!defaultSlug) continue; + + const defaultTitle = titleCase(page); + + // Build slug/title map for all locales + const slugMap = { [defaultLocale]: defaultSlug }; + const titleMap = { [defaultLocale]: defaultTitle }; + + if (secondaryLocales.length > 0) { + if (fastPathNames) { + // argv[3] applies to the first secondary locale; rest default to same slug + const [firstLocale, ...restLocales] = secondaryLocales; + const secName = fastPathNames[idx] ?? page; + slugMap[firstLocale] = slugify(secName) || defaultSlug; + titleMap[firstLocale] = titleCase(secName); + for (const locale of restLocales) { + slugMap[locale] = defaultSlug; + titleMap[locale] = defaultTitle; + } + } else if (ask) { + // Interactive: prompt for each secondary locale + console.log(`\nPage: "${page}" (${defaultLocale} slug: "${defaultSlug}")`); + for (const locale of secondaryLocales) { + const answer = (await ask(` ${locale} name [${page}]: `)).trim(); + const secName = answer || page; + slugMap[locale] = slugify(secName) || defaultSlug; + titleMap[locale] = titleCase(secName); + } + } else { + // Non-TTY with no argv[3]: use default for all secondary locales + for (const locale of secondaryLocales) { + slugMap[locale] = defaultSlug; + titleMap[locale] = defaultTitle; + } + } + } + + // Default locale page + const defaultPagePath = join(root, "src", "pages", `${defaultSlug}.astro`); + if (existsSync(defaultPagePath)) { + console.log(`Skipped src/pages/${defaultSlug}.astro — already exists`); + } else { + writeFileSync(defaultPagePath, applyTemplate(defaultTemplate, defaultTitle), "utf8"); + console.log(`Created src/pages/${defaultSlug}.astro`); + } + + // Secondary locale pages + for (const locale of secondaryLocales) { + const localeSlug = slugMap[locale]; + const localeTitle = titleMap[locale]; + const localePath = join(root, "src", "pages", locale, `${localeSlug}.astro`); + if (existsSync(localePath)) { + console.log(`Skipped src/pages/${locale}/${localeSlug}.astro — already exists`); + } else { + writeFileSync(localePath, applyTemplate(secondaryTemplates[locale], localeTitle), "utf8"); + console.log(`Created src/pages/${locale}/${localeSlug}.astro`); + } + } + + // ── navData.json ─────────────────────────────────────── + const navStatus = registerNavItem(slugMap, titleMap); + + if (navStatus === "registered") { + console.log("Registered in navData.json"); + } + // ── routeTranslations.ts ─────────────────────────────────────────────────── + if (i18nEnabled) { + const rtStatus = registerInRouteTranslations(defaultSlug, slugMap); + + if (rtStatus === "registered") { + console.log("Registered in routeTranslations.ts"); + } else if (rtStatus === "skipped") { + console.log(`Skipped routeTranslations.ts — "${defaultSlug}" already registered`); + } + } + } + + if (rl) rl.close(); } main().catch((err) => { - console.error("Fatal error:", err); - process.exit(1); + console.error("Fatal error:", err); + process.exit(1); }); diff --git a/scripts/remove-dark-mode.js b/scripts/remove-dark-mode.js index 64f535c..fbb9e0c 100644 --- a/scripts/remove-dark-mode.js +++ b/scripts/remove-dark-mode.js @@ -18,10 +18,7 @@ import { existsSync, rmSync, readFileSync, writeFileSync, readdirSync } from "fs import { join, dirname } from "path"; import { fileURLToPath } from "url"; import readline from "readline"; -import { - checkFeatureFlagBeforeRun, - disableFeatureFlag, -} from "./utils/feature-flags.js"; +import { checkFeatureFlagBeforeRun, disableFeatureFlag } from "./utils/feature-flags.js"; import { askYesNo } from "./utils/prompt.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -29,194 +26,182 @@ const root = process.env.SCRIPT_ROOT ?? join(__dirname, ".."); // ─── Guard: already run? ────────────────────────────────────────────────────── if (checkFeatureFlagBeforeRun(root, "darkMode", "Dark Mode")) { - process.exit(0); + process.exit(0); } // ─── Confirmation prompt ────────────────────────────────────────────────────── const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, + input: process.stdin, + output: process.stdout, }); const ask = (q) => new Promise((resolve) => rl.question(q, resolve)); const proceed = await askYesNo( - ask, - "\n⚠️ This will permanently remove dark mode support from the project.\n\nProceed?", - false, + ask, + "\n⚠️ This will permanently remove dark mode support from the project.\n\nProceed?", + false, ); rl.close(); if (!proceed) { - console.log("Aborted. No files were changed."); - process.exit(0); + console.log("Aborted. No files were changed."); + process.exit(0); } await runRemoval(); // ─── Helpers ────────────────────────────────────────────────────────────────── function remove(relPath) { - const abs = join(root, relPath); + const abs = join(root, relPath); - if (existsSync(abs)) { - rmSync(abs, { recursive: true, force: true }); - } + if (existsSync(abs)) { + rmSync(abs, { recursive: true, force: true }); + } } function replace(relPath, from, to) { - const abs = join(root, relPath); + const abs = join(root, relPath); - if (!existsSync(abs)) return; + if (!existsSync(abs)) return; - const before = readFileSync(abs, "utf8"); - const after = before.replaceAll(from, to); + const before = readFileSync(abs, "utf8"); + const after = before.replaceAll(from, to); - if (before !== after) { - writeFileSync(abs, after, "utf8"); - } + if (before !== after) { + writeFileSync(abs, after, "utf8"); + } } function replaceRegex(relPath, pattern, replacement) { - const abs = join(root, relPath); + const abs = join(root, relPath); - if (!existsSync(abs)) return; + if (!existsSync(abs)) return; - const before = readFileSync(abs, "utf8"); - const after = before.replace(pattern, replacement); + const before = readFileSync(abs, "utf8"); + const after = before.replace(pattern, replacement); - if (before !== after) { - writeFileSync(abs, after, "utf8"); - } + if (before !== after) { + writeFileSync(abs, after, "utf8"); + } } /** * Remove all body.dark-mode { ... } blocks. */ function removeDarkModeBlocks(content) { - let result = content; - const selector = "body.dark-mode"; - let idx; + let result = content; + const selector = "body.dark-mode"; + let idx; - while ((idx = result.indexOf(selector)) !== -1) { - const openBrace = result.indexOf("{", idx); + while ((idx = result.indexOf(selector)) !== -1) { + const openBrace = result.indexOf("{", idx); - if (openBrace === -1) break; + if (openBrace === -1) break; - let depth = 1; - let pos = openBrace + 1; + let depth = 1; + let pos = openBrace + 1; - while (pos < result.length && depth > 0) { - if (result[pos] === "{") depth++; - else if (result[pos] === "}") depth--; - pos++; - } + while (pos < result.length && depth > 0) { + if (result[pos] === "{") depth++; + else if (result[pos] === "}") depth--; + pos++; + } - if (result[pos] === "\n") pos++; + if (result[pos] === "\n") pos++; - let start = idx; + let start = idx; - while (start > 0 && result[start - 1] !== "\n") { - start--; - } + while (start > 0 && result[start - 1] !== "\n") { + start--; + } - while (start > 0) { - const lineEnd = start - 1; + while (start > 0) { + const lineEnd = start - 1; - if (result[lineEnd] !== "\n") break; + if (result[lineEnd] !== "\n") break; - let lineStart = lineEnd - 1; + let lineStart = lineEnd - 1; - while (lineStart > 0 && result[lineStart - 1] !== "\n") { - lineStart--; - } + while (lineStart > 0 && result[lineStart - 1] !== "\n") { + lineStart--; + } - const line = result.slice(lineStart, lineEnd).trim(); + const line = result.slice(lineStart, lineEnd).trim(); - if (line === "" || /^\/\*.*\*\/$/.test(line)) { - start = lineStart; - } else { - break; - } - } + if (line === "" || /^\/\*.*\*\/$/.test(line)) { + start = lineStart; + } else { + break; + } + } - result = result.slice(0, start) + result.slice(pos); - } + result = result.slice(0, start) + result.slice(pos); + } - result = result.replace(/\n[ \t]*@media[^{]+\{\s*\}[ \t]*/g, ""); - result = result.replace(/\n[ \t]*\/\* [Dd]ark [Mm]ode[^*]*\*\/[ \t]*/gi, ""); + result = result.replace(/\n[ \t]*@media[^{]+\{\s*\}[ \t]*/g, ""); + result = result.replace(/\n[ \t]*\/\* [Dd]ark [Mm]ode[^*]*\*\/[ \t]*/gi, ""); - return result; + return result; } /** * Recursively collect matching files. */ function walkFiles(dir, exts) { - const files = []; + const files = []; - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const full = join(dir, entry.name); + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); - if (entry.isDirectory()) { - files.push(...walkFiles(full, exts)); - } else if (exts.some((ext) => entry.name.endsWith(ext))) { - files.push(full); - } - } + if (entry.isDirectory()) { + files.push(...walkFiles(full, exts)); + } else if (exts.some((ext) => entry.name.endsWith(ext))) { + files.push(full); + } + } - return files; + return files; } // ─── Main ───────────────────────────────────────────────────────────────────── async function runRemoval() { - console.log("\nRemoving dark mode support...\n"); - - // ── Remove DarkMode components ──────────────────────────────────────────── - remove("src/features/darkmode"); - - // ── Update Settings ─────────────────────────────────────────────────────── - const settingsPath = "src/components/Settings/Settings.astro"; - - replaceRegex( - settingsPath, - /import ThemeSelect from "src\/features\/darkmode\/ThemeSelect\.astro";\r?\n/, - "" - ); - - replaceRegex( - settingsPath, - /\s*\r?\n?/, - "" - ); - - // ── Remove dark mode inline scripts ─────────────────────────────────────── - replaceRegex( - "src/layouts/BaseLayout.astro", - /\n\t\t diff --git a/src/components/Footer/Footer.astro b/src/components/Footer/Footer.astro index ae654fa..0bfa467 100644 --- a/src/components/Footer/Footer.astro +++ b/src/components/Footer/Footer.astro @@ -12,369 +12,352 @@ const currentYear = getCurrentYear(); --- diff --git a/src/components/Header/DynamicHeader.astro b/src/components/Header/DynamicHeader.astro index 317e25c..9c30646 100644 --- a/src/components/Header/DynamicHeader.astro +++ b/src/components/Header/DynamicHeader.astro @@ -11,17 +11,8 @@ const { locale, content } = await getSiteContext(Astro.url);
-