Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose FS translation system to plugins #2578

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

HiDeoo
Copy link
Member

@HiDeoo HiDeoo commented Nov 5, 2024

Description

This plugin exposes the file system translation system to Starlight plugins throught the setup hook using a new useTranslations() callback function.

This change requires the addition of a new plugin hook (currently named init, I picked the first thing coming to mind to get started) which now exposes the injectTranslations() callback function (which was previously available in the setup hook).

The documentation so far is only a rough draft to get an idea of everything changed.

Remaining tasks

  • Restore the docs/astro.config.mjs file
  • Restore the docs/package.json file
  • Restore the docs/src/content/i18n/zh-CN.json file
  • Remove the entire packages/demo-plugin/ directory
  • Run pnpm i to remove mention of the starlight-demo-plugin package in the lockfile
  • Remove all remaining // TODO(HiDeoo) comments (if any)

Copy link

changeset-bot bot commented Nov 5, 2024

🦋 Changeset detected

Latest commit: 36ef34f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@astrojs/starlight Minor
starlight-demo-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added 📚 docs Documentation website changes 🌟 core Changes to Starlight’s main package labels Nov 5, 2024
Copy link

netlify bot commented Nov 5, 2024

Deploy Preview for astro-starlight ready!

Name Link
🔨 Latest commit 36ef34f
🔍 Latest deploy log https://app.netlify.com/sites/astro-starlight/deploys/67320f48d9677500080ed596
😎 Deploy Preview https://deploy-preview-2578--astro-starlight.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 100 (no change from production)
Accessibility: 100 (no change from production)
Best Practices: 100 (no change from production)
SEO: 100 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify site configuration.

@@ -152,7 +151,7 @@ export function getStarlightEcConfigPreprocessor({
},
...otherStyleOverrides,
},
getBlockLocale: ({ file }) => pathToLocale(file.path, { starlightConfig, astroConfig }),
getBlockLocale: ({ file }) => pathToLang(file.path, { astroConfig, starlightConfig }),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expressive Code technically expects a Starlight language here so I changed it but this is not a bug fix (no changeset) as Expressive Code lower-cases the language identifier anyway.

@astrobot-houston
Copy link
Collaborator

astrobot-houston commented Nov 5, 2024

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

Locale File Note
en guides/i18n.mdx Source changed, localizations will be marked as outdated.
en reference/plugins.md Source changed, localizations will be marked as outdated.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

Copy link
Member

@delucis delucis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great @HiDeoo! Left some suggestions/questions/discussions, but they’re honestly all around the “outside” of the core functionality or on the docs. I think overall this seems very solid 🚀

name: 'plugin-with-translations',
hooks: {
- setup({ injectTranslations }) {
+ init({ injectTranslations }) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the hook naming, I was wondering if we should follow the colon-separated pattern used by Astro integrations? Both of these hooks are essentially “setup” hooks, so the new hook would become i18n:setup() or translations:setup() or something. i18n is nice because it’s short, but maybe a bit misleading if you don’t set up i18n config here, only translations.

Ideally maybe we’d even align both hooks like this, although that’s a bit of an annoying breaking change to impose on everyone:

interface Plugin {
  name: string;
  hooks: {
    'i18n:setup': (opts: I18nSetupOptions) => void;
    'config:setup': (opts: ConfigSetupOptions) => void;
  }
}

That said, I don’t know if there’s a future where this starts to be silly. Like is there anything Starlight would do/need that is not “setup”? I guess we might have a config:done hook like Astro does that gives access to the final resolved Starlight config?

I guess the flipside of an i18n-focused naming is if we think this “early” initialization phase is going to be used for other things (which your current docs kind of suggests: “initialization function called before Starlight is initialized (during the astro:config:setup integration hook)”). In that case something more focused on lifecycle like pre:setup (😅) or whatever could make sense. I find it hard to evaluate the probability of that, so open to hear opinions. The i18n approach does kind of help communicate use case up front as well, so maybe it’s OK.


(If we do rename setup() it doesn’t have to be in this PR and can be done in a backwards compatible way where we still call setup() if defined but mark it as deprecated.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the hook naming, I was wondering if we should follow the colon-separated pattern used by Astro integrations?

I personally like that idea.

but maybe a bit misleading if you don’t set up i18n config here, only translations.

I guess, altho if we compare with our configuration file, we use the locales key to configure i18n, not i18n so I think it's fine.

I guess we might have a config:done hook like Astro does that gives access to the final resolved Starlight config?

Maybe, I don't really see the need for it right now and I'm not even sure what you would do with it.

which your current docs kind of suggests: “initialization function called before Starlight is initialized (during the astro:config:setup integration hook)”

Well, to be fair, like I said, I didn't really put any thought at all about the naming of the hook, and I documented it mostly to give an overview of all the changes and mimick what we had before.

I think if we ever need something else to be done before config:setup that is not i18n, we could still add an xyz:setup and call it before config:setup like we do with i18n:setup and it would be fine.

If we do rename setup() it doesn’t have to be in this PR and can be done in a backwards compatible way where we still call setup() if defined but mark it as deprecated.

I think we maybe do want to do that at the same time, would be easier to communicate and organize docs? And we can still soft deprecate the setup method in the code so it's still run.

Copy link
Member

@delucis delucis Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, that sounds good! So if you think i18n is not problematic, I think it’s nice to have that concise hook name. And +1 to do the backwards compatible deprecation of setup in favour of config:setup.

Regarding what you might do with a config:done: most often I see the astro:config:done hook used to share the final config with some other code via the integration closure, e.g.

function integration() {
  const lazy: { config?: AstroConfig } = {};
  return {
    name: 'example',
    hooks: {
      'astro:config:setup'() {
        // add something referencing `lazy` e.g. a Vite plugin
        // that will run _after_ the done hook but wants the
        // final config shape
      },
      'astro:config:done'({config}) {
        lazy.config = config;
      },
    },
  };
}

But not suggesting we add that now. No point until some more clear needs emerge.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect, I'll get started asap on the new changes, thanks again.

@@ -63,6 +59,34 @@ export default defineVitestConfig({
},
});
},
async setup({ config, updateConfig, useTranslations, pathToLang }) {
await Promise.resolve();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this here for a reason or is it just left over from some debugging?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh… I see it was already there in the previous version of this hook. I guess my question still stands though 😅

Maybe it’s ensuring the hook is really async to test that? Whatever it is, if it’s important to keep, it’s probably a good idea to add a comment for next time I’m reviewing this and confused.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a comment, this is mostly to ensure type-wise it supports async setup() while avoiding to add a type test for it as type tests are still a bit broken within a Vitest workspace setup like we have.

import type { createTranslationSystemFromFs } from '../../utils/translations-fs';
import { pathToLocale } from '../shared/pathToLocale';
import type { StarlightConfig, StarlightPlugin } from '../../types';
import { pathToLang } from '../shared/pathToLang';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remind me why EC uses this rather than the one passed from the plugins instance?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this deserves a comment somewhere, but it's for the <Code> component basically which runs in a different context.

// strings. We let TypeScript merge them into a single union type so that plugins with a TypeScript
// configuration preventing `UserI18nKeys` to be properly inferred can still get auto-completion
// for built-in UI strings.
export type I18nKeys = keyof BuiltInStrings | UserI18nKeys | keyof StarlightApp.I18n;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhhhhh, nice fix! 🧠

Hooks are functions which Starlight calls to run plugin code at specific times. Currently, Starlight supports a single `setup` hook.
Hooks are functions which Starlight calls to run plugin code at specific times.

### `hooks.init`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do adopt the colon-style of naming, I’d probably recommend removing hooks. in these subheadings so we just have something like :

## `hooks`

...

### `i18n:setup`

...

### `config:setup`

...

Comment on lines 54 to 55
Plugin initialization function called before Starlight is initialized (during the [`astro:config:setup`](https://docs.astro.build/en/reference/integrations-reference/#astroconfigsetup) integration hook).
The `init` hook can currently only be used to inject translations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do go with an i18n-focused hook name, I’m sure we can clean this up (and maybe makes it easier to explain as well vs a hook for “initializing before stuff is initialized”). Will hold off suggesting anything while we discuss the hook naming.

Comment on lines 252 to 253
A callback function to generate a utility function to access UI strings for a given language.
To learn more about the generated utility function and all the APIs available, see the [“Using UI translations”](/guides/i18n/#using-ui-translations) guide.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggesting some tweaks to avoid some of the repeated use of “function” and make it a bit clearer that the return is the same as t() rather than just a “generated utility function” which I think communicates a bit less what this does.

Suggested change
A callback function to generate a utility function to access UI strings for a given language.
To learn more about the generated utility function and all the APIs available, see the [“Using UI translations”](/guides/i18n/#using-ui-translations) guide.
Call `useTranslations()` with a language tag to generate a utility function that provides access to UI strings for that language.
`useTranslations()` returns an equivalent of the `Astro.locals.t()` API that is available in Astro components.
To learn more about the available APIs, see the [“Using UI translations”](/guides/i18n/#using-ui-translations) guide.


You can also infer the types for the `StarlightApp.I18n` interface from a source file if you have an object containing your translations.
A callback function to get the language for a given full file path.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aligning this with the previous suggestion, which I also like because it makes clear the arguments to pass as well as the purpose.

Suggested change
A callback function to get the language for a given full file path.
Call `pathToLang()` with a file path to get the language for that file.

One question here though: in general these docs always say “full” file path. What happens if I do pathToLang('src/content/docs/en/foo.md')? Or even pathToLang('en/foo.md')? Is it dependent on being an absolute path?

Copy link
Member Author

@HiDeoo HiDeoo Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add a few test cases for these, but they all fallback to the default locale. I tried to use the same naming convention (full path) as the one used in the vfile docs for file.path as this would be I assume on the of the most common scenario.

Copy link
Member

@delucis delucis Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I noticed “full path” used in the vFile docs when I clicked through too. Personally, I find that term a little unclear, vs something like “absolute path” which I think is the same as what they mean by “full”?

I’m almost tempted to suggest the helper should even be called absolutePathToLang() to drive this home. Or to have some less friendly behaviour like erroring if the path doesn’t match what we expect (so people don’t get silent fallback locales without realising they used it wrong), although I don’t know if that’s possible technically. Basically I want to avoid people doing pathToLang('en/foo') and expecting it to work/not noticing it isn’t working.

Comment on lines 280 to 282
This can be particularly useful in [remark or rehype plugins](https://docs.astro.build/en/guides/markdown-content/#markdown-plugins) that a Starlight plugin may use to process Markdown or MDX files.
The [virtual file format](https://github.com/vfile/vfile) used by such plugins includes the [full path](https://github.com/vfile/vfile#filepath) of the file being processed, which can be used with `pathToLang` to determine the language of the file.
The returned language can be used with the [`useTranslations`](#usetranslations) helper to get UI strings for that language.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggesting some tiny tweaks here:

  • removed the “Starlight plugin” part as it helps simplify the sentence and hopefully it’s obvious on this page we’re referring to plugins
  • added () to the mention of the functions like we usually do
Suggested change
This can be particularly useful in [remark or rehype plugins](https://docs.astro.build/en/guides/markdown-content/#markdown-plugins) that a Starlight plugin may use to process Markdown or MDX files.
The [virtual file format](https://github.com/vfile/vfile) used by such plugins includes the [full path](https://github.com/vfile/vfile#filepath) of the file being processed, which can be used with `pathToLang` to determine the language of the file.
The returned language can be used with the [`useTranslations`](#usetranslations) helper to get UI strings for that language.
This can be particularly useful when adding [remark or rehype plugins](https://docs.astro.build/en/guides/markdown-content/#markdown-plugins) to process Markdown or MDX files.
The [virtual file format](https://github.com/vfile/vfile) used by these plugins includes the [full path](https://github.com/vfile/vfile#filepath) of the file being processed, which can be used with `pathToLang()` to determine the language of the file.
The returned language can be used with the [`useTranslations()`](#usetranslations) helper to get UI strings for that language.

@HiDeoo
Copy link
Member Author

HiDeoo commented Nov 8, 2024

Thanks for the review 🙌

Holding off on the suggested changes so I can tackle them all at once when we have a clearer idea of what we want to do regarding the name.

HiDeoo and others added 12 commits November 9, 2024 17:46
* main: (27 commits)
  i18n(ko-KR): update `site-search.mdx`
  [ci] release (withastro#2590)
  Support passing more options to the DocSearch component (withastro#2589)
  [ci] release (withastro#2587)
  Add changeset for withastro#2252 (withastro#2588)
  [ci] format
  Update dev dependencies (withastro#2582)
  Build performance optimizations for projects with large sidebars (withastro#2252)
  Fix a11y CI workflow (withastro#2503)
  Update `astro-expressive-code` to v0.38 (withastro#2551)
  Added social icon for Nostr (withastro#2579)
  [ci] format
  docs(showcase): add docs.reactbricks.com to showcase (withastro#2586)
  i18n(ja): Update site-search.mdx (withastro#2577)
  i18n(ja): Update pages.mdx (withastro#2576)
  i18n(fr): Update `reference/plugins.mdx` from withastro#2549 (withastro#2574)
  [ci] format
  docs: update showcase-sites.astro (withastro#2562)
  Throw an error if a showcase image does not have the required dimensions (withastro#2573)
  i18n(ja): Update frontmatter.md (withastro#2566)
  ...
@HiDeoo
Copy link
Member Author

HiDeoo commented Nov 11, 2024

Updated the branch with the changes discussed above, hope I did not forget anything:

  • Adds the config:setup hook and deprecate the setup one
    • I added a few tests to ensure we make sure to totally remove setup before v1
  • Renames the new hook to i18n:setup
  • Cleans up and renames the new path utility to absolutePathToLang
    • It's fairly easy to add some checks to ensure the paths are the ones we expect for a document in a content collection altho, this would fail as soon as something like import { Content } from '../../something.md' is used (this would even fail with our EmptyMarkdown file).
    • Should we explicitly mention that if it's not matching a configured locale, it's falling back to the default locale?
  • Adds a test to ensure we migrate @astrojs/starlight-docsearch to the config:setup hook before v1
  • Adds a new HookParameters utility type and its documentation
    • A lot of plugin are already doing something like Parameters<StarlightPlugin['hooks']['setup']>['0']['something'] but now this would be even longer with the addition of a nullable so the new type should help with that like in Astro.
  • Updates starlight-links-validator to a new version where I refactored some very old legacy types to avoid type errors
  • Updates the useTranslations() documentation & example to mention it's a BCP-47 tag and to use a region subtag in the example

I have a question: this PR adds multiple changesets but I think .changeset/polite-fishes-remain.md should come first. Altho, I don't know if there is a way to enforce that from this PR, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🌟 core Changes to Starlight’s main package 📚 docs Documentation website changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants