diff --git a/.betterer.eslint.config.js b/.betterer.eslint.config.js
index 1be44601a3c..349ee612837 100644
--- a/.betterer.eslint.config.js
+++ b/.betterer.eslint.config.js
@@ -113,6 +113,7 @@ module.exports = [
ignores: ['public/app/plugins/**', '**/*.story.tsx', '**/*.{test,spec}.{ts,tsx}', '**/__mocks__/', 'public/test'],
rules: {
'@grafana/no-untranslated-strings': 'error',
+ '@grafana/no-translation-top-level': 'error',
},
},
];
diff --git a/.betterer.results b/.betterer.results
index 09d20205df6..b889576f7fe 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -549,6 +549,11 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
+ "packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.tsx:5381": [
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "0"],
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "1"],
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "2"]
+ ],
"packages/grafana-ui/src/components/DataSourceSettings/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
@@ -3047,7 +3052,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"]
],
"public/app/features/connections/tabs/ConnectData/Search/Search.tsx:5381": [
- [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"]
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "0"],
+ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"]
],
"public/app/features/connections/tabs/ConnectData/Search/index.tsx:5381": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"]
@@ -4621,9 +4627,18 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with ", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with ", "3"]
],
+ "public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx:5381": [
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "0"],
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "1"],
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "2"],
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "3"]
+ ],
"public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with ", "0"]
],
+ "public/app/features/explore/ShortLinkButtonMenu.tsx:5381": [
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "0"]
+ ],
"public/app/features/explore/SupplementaryResultError.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with ", "0"]
],
@@ -4982,8 +4997,14 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with ", "4"]
],
"public/app/features/inspector/InspectJSONTab.tsx:5381": [
- [0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "1"]
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "0"],
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "1"],
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "2"],
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "3"],
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "4"],
+ [0, 0, 0, "Do not use the t() function outside of a component or function", "5"],
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "6"],
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "7"]
],
"public/app/features/inspector/InspectStatsTab.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index e9d98be067b..ada306f734d 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -622,7 +622,7 @@ playwright.config.ts @grafana/plugins-platform-frontend
/scripts/import_many_dashboards.sh @torkelo
/scripts/mixin-check.sh @bergquist
/scripts/openapi3/ @grafana/grafana-operator-experience-squad
-/scripts/prepare-packagejson.js @grafana/frontend-ops
+/scripts/prepare-npm-package.js @grafana/frontend-ops
/scripts/protobuf-check.sh @grafana/plugins-platform-backend
/scripts/stripnulls.sh @grafana/grafana-as-code
/scripts/tag_release.sh @grafana/grafana-developer-enablement-squad
@@ -820,6 +820,7 @@ embed.go @grafana/grafana-as-code
/.github/workflows/changelog.yml @zserge
/.github/workflows/actions/changelog @zserge
/.github/workflows/frontend-unit-tests.yml @grafana/grafana-frontend-platform
+/.github/workflows/frontend-lint.yml @grafana/grafana-frontend-platform
# Generated files not requiring owner approval
/packages/grafana-data/src/types/featureToggles.gen.ts @grafanabot
diff --git a/.github/workflows/frontend-lint.yml b/.github/workflows/frontend-lint.yml
new file mode 100644
index 00000000000..a9b65b247fb
--- /dev/null
+++ b/.github/workflows/frontend-lint.yml
@@ -0,0 +1,55 @@
+name: Lint Frontend
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+ - release-*.*.*
+
+jobs:
+ verify-i18n:
+ name: Verify i18n
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'yarn'
+ cache-dependency-path: 'yarn.lock'
+ - run: yarn install --immutable --check-cache
+ - run: |
+ extract_error_message='::error::Extraction failed. Make sure that you have no dynamic translation phrases, such as "t(`preferences.theme.{themeID}`, themeName)" and that no translation key is used twice. Search the output for '[warning]' to find the offending file.'
+ make i18n-extract || (echo "${extract_error_message}" && false)
+ - run: |
+ uncommited_error_message="::error::Translation extraction has not been committed. Please run 'make i18n-extract', commit the changes and push again."
+ file_diff=$(git diff --dirstat public/locales)
+ if [ -n "$file_diff" ]; then
+ echo $file_diff
+ echo "${uncommited_error_message}"
+ exit 1
+ fi
+ prettier:
+ name: Prettier
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'yarn'
+ cache-dependency-path: 'yarn.lock'
+ - run: yarn install --immutable --check-cache
+ - run: yarn run prettier:check
+ typecheck:
+ name: Typecheck
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'yarn'
+ cache-dependency-path: 'yarn.lock'
+ - run: yarn install --immutable --check-cache
+ - run: yarn run typecheck
diff --git a/.gitignore b/.gitignore
index b8618dff2be..858a241fcf9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -151,6 +151,7 @@ pkg/services/quota/quotaimpl/storage/storage.json
# Ignoring frontend packages specifics
/packages/grafana-ui/.yarn/.cache
/packages/grafana-ui/.storybook/static
+/packages/grafana-ui/unstable
/packages/**/dist
/packages/**/compiled
/packages/**/.rpt2_cache
diff --git a/.vale.ini b/.vale.ini
index cf4c71392dc..37e764e6d0f 100644
--- a/.vale.ini
+++ b/.vale.ini
@@ -1,5 +1,5 @@
MinAlertLevel = warning
-[*]
+[*.md]
BasedOnStyles = Grafana
TokenIgnores = (+?), \*\*[^\n]+\*\*
diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md
index 601ae34e4e1..8362360d188 100644
--- a/docs/sources/alerting/_index.md
+++ b/docs/sources/alerting/_index.md
@@ -25,7 +25,7 @@ cards:
items:
- title: Introduction
href: ./fundamentals/
- description: Learn more about the fundamentals and available features that help you create, manage, and respond to alerts; and improve your team’s ability to resolve issues quickly.
+ description: Learn more about the fundamentals and available features that help you create, manage, and respond to alerts; and improve your team's ability to resolve issues quickly.
height: 24
- title: Configure alert rules
href: ./alerting-rules/
@@ -55,7 +55,8 @@ Monitor your incoming metrics data or log entries and set up your Grafana Alerti
In this way, you eliminate the need for manual monitoring and provide a first line of defense against system outages or changes that could turn into major incidents.
-Using Grafana Alerting, you create queries and expressions from multiple data sources — no matter where your data is stored — giving you the flexibility to combine your data and alert on your metrics and logs in new and unique ways. You can then create, manage, and take action on your alerts from a single, consolidated view, and improve your team’s ability to identify and resolve issues quickly.
+Using Grafana Alerting, you create queries and expressions from multiple data sources, no matter where you store your data, giving you the flexibility to combine your data and alert on your metrics and logs in new and unique ways.
+You can then create, manage, and take action on your alerts from a single, consolidated view, and improve your team's ability to identify and resolve issues quickly.
## Explore
diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md
index 41c9150b52c..3b30819b328 100644
--- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md
+++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md
@@ -118,15 +118,17 @@ refs:
# Configure Grafana-managed alert rules
-Grafana-managed rules can query data from multiple data sources in a single alert rule. They are the most flexible [alert rule type](ref:alert-rules). You can also add expressions to transform your data, set alert conditions, and images in alert notifications.
+Grafana-managed rules can query data from multiple data sources in a single alert rule.
+They're the most flexible [alert rule type](ref:alert-rules).
+You can also add expressions to transform your data, set alert conditions, and images in alert notifications.
-{{% admonition type="note" %}}
+{{< admonition type="note" >}}
In Grafana Cloud, the number of Grafana-managed alert rules you can create depends on your Grafana Cloud plan.
- Free Forever plan: You can create up to 100 free alert rules, with each alert rule having a maximum of 1000 alert instances.
- All paid plans (Pro and Advanced): They have a soft limit of 2000 alert rules and support unlimited alert instances. To increase the limit, open a support ticket from the [Cloud portal](/docs/grafana-cloud/account-management/support/).
-{{% /admonition %}}
+{{< /admonition >}}
To create or edit Grafana-managed alert rules, follow the instructions below. For a practical example, check out our [tutorial on getting started with Grafana alerting](http://grafana.com/tutorials/alerting-get-started/).
@@ -144,9 +146,11 @@ Only users with **Edit** permissions for the folder storing the rules can edit o
You can use default or advanced options for Grafana-managed alert rule creation. The default options streamline rule creation with a cleaner header and a single query and condition. For more complex rules, use advanced options to add multiple queries and expressions.
-You can toggle between the two options. Once you have created an alert rule, the system defaults to your previous choice for the next alert rule.
+You can toggle between the two options.
+After you have created an alert rule, the system defaults to your previous choice for the next alert rule.
-Switching from advanced to default may result in queries and expressions that cannot be converted. In this case, a warning message asks if you want to continue to reset to default settings.
+Switching from advanced to default may result in queries and expressions that can't be converted.
+In this case, a warning message asks if you want to continue to reset to default settings.
{{< docs/shared lookup="alerts/configure-alert-rule-name.md" source="grafana" version="" >}}
@@ -169,9 +173,10 @@ You can toggle between **Default** and **Advanced** options. If the [Default vs.
{{< collapse title="Advanced options" >}}
1. Select a data source.
-1. From the **Options** dropdown, specify a [time range](ref:time-units-and-relative-ranges).
+1. From the **Options** drop-down menu, specify a [time range](ref:time-units-and-relative-ranges).
- Note that Grafana Alerting only supports fixed relative time ranges, for example, `now-24hr: now`. It does not support absolute time ranges: `2021-12-02 00:00:00 to 2021-12-05 23:59:592` or semi-relative time ranges: `now/d to: now`.
+ Note that Grafana Alerting only supports fixed relative time ranges, for example, `now-24hr: now`.
+ It doesn't support absolute time ranges: `2021-12-02 00:00:00 to 2021-12-05 23:59:592` or semi-relative time ranges: `now/d to: now`.
1. Add a query.
@@ -222,7 +227,8 @@ To do this, you need to make sure that your alert rule is in the right evaluatio
The pending period is the period in which an alert rule can be in breach of the condition until it fires.
- Once a condition is met, the alert goes into the **Pending** state. If the condition remains active for the duration specified, the alert transitions to the **Firing** state, else it reverts to the **Normal** state.
+ After a condition is met, the alert goes into the **Pending** state.
+ If the condition remains active for the duration specified, the alert transitions to the **Firing** state, else it reverts to the **Normal** state.
1. Turn on pause alert notifications, if required.
@@ -253,7 +259,7 @@ Complete the following steps to set up notifications.
1. Choose this option to select an existing [contact point](ref:contact-points).
- All notifications for this alert rule are sent to this contact point automatically and notification policies are not used.
+ All notifications for this alert rule are sent to this contact point automatically and notification policies aren't used.
1. You can also optionally select a mute timing as well as groupings and timings to define when not to send notifications.
@@ -261,7 +267,8 @@ Complete the following steps to set up notifications.
1. Choose this option to use the [notification policy tree](ref:notification-policies) to handle alert notifications.
- All notifications for this alert rule are managed by the notification policy tree, which routes alerts based on their labels. If an alert does not match a specific policy, the default notification policy applies, ensuring all alerts are handled.
+ All notifications for this alert rule are managed by the notification policy tree, which routes alerts based on their labels.
+ If an alert doesn't match a specific policy, the default notification policy applies, ensuring all alerts are handled.
1. Preview your alert instance routing set up.
diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
index f40ad6a1766..7ceb0dc3460 100644
--- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
+++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
@@ -226,6 +226,7 @@ Experimental features might be changed or removed without prior notice.
| `datasourceConnectionsTab` | Shows defined connections for a data source in the plugins detail page |
| `newLogsPanel` | Enables the new logs panel in Explore |
| `pluginsCDNSyncLoader` | Load plugins from CDN synchronously |
+| `assetSriChecks` | Enables SRI checks for Grafana JavaScript assets |
## Development feature toggles
diff --git a/docs/sources/shared/plugins/rename-note.md b/docs/sources/shared/plugins/rename-note.md
index 5eaa9a1adf4..4fec1abb499 100644
--- a/docs/sources/shared/plugins/rename-note.md
+++ b/docs/sources/shared/plugins/rename-note.md
@@ -15,5 +15,5 @@ labels:
{{< admonition type="note" >}}
The Grafana Explore apps have changed to Grafana Drilldown apps.
For example, Explore Logs is now Logs Drilldown.
-To learn more, read [Grafana Drilldown apps: the improved queryless experience known as the Explore apps](https://grafana.com/blog/2025/02/20/grafana-drilldown-apps-the-improved-queryless-experience-formerly-known-as-the-explore-apps/).
+To learn more, read [Grafana Drilldown apps: the improved queryless experience formerly known as the Explore apps](https://grafana.com/blog/2025/02/20/grafana-drilldown-apps-the-improved-queryless-experience-formerly-known-as-the-explore-apps/).
{{< /admonition >}}
diff --git a/package.json b/package.json
index 1660de6b891..3708b2b49ac 100644
--- a/package.json
+++ b/package.json
@@ -83,6 +83,7 @@
"@grafana/plugin-e2e": "1.17.1",
"@grafana/tsconfig": "^2.0.0",
"@manypkg/get-packages": "^2.2.0",
+ "@npmcli/package-json": "^5.2.0",
"@playwright/test": "1.50.1",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.15",
"@react-types/button": "3.10.2",
@@ -247,6 +248,7 @@
"webpack-livereload-plugin": "3.0.2",
"webpack-manifest-plugin": "5.0.0",
"webpack-merge": "6.0.1",
+ "webpack-subresource-integrity": "^5.2.0-rc.1",
"webpackbar": "^7.0.0",
"yaml": "^2.0.0",
"yargs": "^17.5.1"
diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json
index 61124640e72..c33053087f2 100644
--- a/packages/grafana-data/package.json
+++ b/packages/grafana-data/package.json
@@ -16,9 +16,9 @@
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
- "main": "dist/index.js",
- "module": "dist/esm/index.js",
- "types": "dist/index.d.ts",
+ "main": "./dist/cjs/index.cjs",
+ "module": "./dist/esm/index.mjs",
+ "types": "./dist/cjs/index.d.cts",
"access": "public"
},
"files": [
@@ -29,10 +29,10 @@
],
"scripts": {
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild",
- "clean": "rimraf ./dist ./compiled ./package.tgz",
+ "clean": "rimraf ./dist ./compiled ./unstable ./package.tgz",
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
- "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js",
- "postpack": "mv package.json.bak package.json"
+ "prepack": "cp package.json package.json.bak && ALIAS_PACKAGE_NAME=unstable node ../../scripts/prepare-npm-package.js",
+ "postpack": "mv package.json.bak package.json && rimraf ./unstable"
},
"dependencies": {
"@braintree/sanitize-url": "7.0.1",
diff --git a/packages/grafana-data/rollup.config.ts b/packages/grafana-data/rollup.config.ts
index 5a913449466..f67f12e07d0 100644
--- a/packages/grafana-data/rollup.config.ts
+++ b/packages/grafana-data/rollup.config.ts
@@ -11,5 +11,23 @@ export default [
plugins,
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-data')],
},
+ {
+ input: 'src/unstable.ts',
+ plugins,
+ output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-data')],
+ },
tsDeclarationOutput(pkg),
+ tsDeclarationOutput(pkg, {
+ input: './compiled/unstable.d.ts',
+ output: [
+ {
+ file: './dist/cjs/unstable.d.cts',
+ format: 'cjs',
+ },
+ {
+ file: './dist/esm/unstable.d.mts',
+ format: 'es',
+ },
+ ],
+ }),
];
diff --git a/packages/grafana-data/src/transformations/transformDataFrame.ts b/packages/grafana-data/src/transformations/transformDataFrame.ts
index 1a923ec0d86..35951f9f5f6 100644
--- a/packages/grafana-data/src/transformations/transformDataFrame.ts
+++ b/packages/grafana-data/src/transformations/transformDataFrame.ts
@@ -1,3 +1,4 @@
+import { cloneDeep } from 'lodash';
import { MonoTypeOperatorFunction, Observable, of } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
@@ -12,9 +13,6 @@ import {
import { getFrameMatchers } from './matchers';
import { standardTransformersRegistry, TransformerRegistryItem } from './standardTransformersRegistry';
-// when running within Scenes, we can skip var interpolation, since it's already handled upstream
-const isScenes = window.__grafanaSceneContext != null;
-
const getOperator =
(config: DataTransformerConfig, ctx: DataTransformContext): MonoTypeOperatorFunction =>
(source) => {
@@ -27,9 +25,12 @@ const getOperator =
const defaultOptions = info.transformation.defaultOptions ?? {};
const options = { ...defaultOptions, ...config.options };
+ // when running within Scenes, we can skip var interpolation, since it's already handled upstream
+ const isScenes = window.__grafanaSceneContext != null;
+
const interpolated = isScenes
? options
- : deepIterate(options, (v) => {
+ : deepIterate(cloneDeep(options), (v) => {
if (typeof v === 'string') {
return ctx.interpolate(v);
}
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index 87c5ff7c0d1..7e3ac0d6dba 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -254,4 +254,5 @@ export interface FeatureToggles {
alertingRuleVersionHistoryRestore?: boolean;
newShareReportDrawer?: boolean;
rendererDisableAppPluginsPreload?: boolean;
+ assetSriChecks?: boolean;
}
diff --git a/packages/grafana-data/src/unstable.ts b/packages/grafana-data/src/unstable.ts
new file mode 100644
index 00000000000..8a42447206f
--- /dev/null
+++ b/packages/grafana-data/src/unstable.ts
@@ -0,0 +1,13 @@
+/**
+ * THESE APIS MUST NOT BE USED IN COMMUNITY PLUGINS.
+ *
+ * Unstable APIs are still in development and are subject to breaking changes
+ * at any point, like feature flags but for APIS. They must only be used in
+ * Grafana core and internal plugins where we can coordinate changes.
+ *
+ * Once mature, they will be moved to the main export, be available to plugins via the standard import path,
+ * and be subject to the standard policies
+ */
+
+// This is a dummy export so typescript doesn't error importing an "empty module"
+export const unstable = {};
diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json
index 739e36cfec8..dc8a0541bcf 100644
--- a/packages/grafana-e2e-selectors/package.json
+++ b/packages/grafana-e2e-selectors/package.json
@@ -19,9 +19,9 @@
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
- "main": "dist/index.js",
- "module": "dist/esm/index.js",
- "types": "dist/index.d.ts",
+ "main": "./dist/cjs/index.cjs",
+ "module": "./dist/esm/index.mjs",
+ "types": "./dist/cjs/index.d.cts",
"access": "public"
},
"files": [
@@ -35,7 +35,7 @@
"bundle": "rollup -c rollup.config.ts --configPlugin esbuild",
"clean": "rimraf ./dist ./compiled ./package.tgz",
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
- "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js",
+ "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
"postpack": "mv package.json.bak package.json"
},
"devDependencies": {
@@ -51,7 +51,7 @@
},
"dependencies": {
"@grafana/tsconfig": "^2.0.0",
- "semver": "7.7.0",
+ "semver": "^7.7.0",
"tslib": "2.8.1",
"typescript": "5.7.3"
}
diff --git a/packages/grafana-eslint-rules/README.md b/packages/grafana-eslint-rules/README.md
index 2f73d46a529..e57e2ec09a1 100644
--- a/packages/grafana-eslint-rules/README.md
+++ b/packages/grafana-eslint-rules/README.md
@@ -156,7 +156,18 @@ const SearchTitle = ({ term }) => (
#### How to translate props or attributes
-Right now, we only check if a string is wrapped up by the `Trans` tag. We currently do not apply this rule to props, attributes or similar, but we also ask for them to be translated with the `t()` function.
+This rule checks if a string is wrapped up by the `Trans` tag, or if certain props contain untranslated strings.
+We ask for such props to be translated with the `t()` function.
+
+The below props are checked for untranslated strings:
+
+- `label`
+- `description`
+- `placeholder`
+- `aria-label`
+- `title`
+- `text`
+- `tooltip`
```tsx
// Bad ❌
@@ -168,3 +179,50 @@ return ;
```
Check more info about how translations work in Grafana in [Internationalization.md](https://github.com/grafana/grafana/blob/main/contribute/internationalization.md)
+
+### `no-translation-top-level`
+
+Ensure that `t()` translation method is not used at the top level of a file, outside of a component of method.
+This is to prevent calling the translation method before it's been instantiated.
+
+This does not cause an error if a file is lazily loaded, but refactors can cause errors, and it can cause problems in tests.
+Fix the
+
+```tsx
+// Bad ❌
+const someTranslatedText = t('some.key', 'Some text');
+const SomeComponent = () => {
+ return ;
+};
+
+// Good ✅
+const SomeComponent = () => {
+ const someTranslatedText = t('some.key', 'Some text');
+ return ;
+};
+
+// Bad ❌
+const someConfigThatHasToBeShared = [{ foo: t('some.key', 'Some text') }];
+const SomeComponent = () => {
+ return (
+