Frontend Development 22 min read

Preventing Day.js Locale Pollution in Large Frontend Projects with Static Analysis

This article recounts a date‑misalignment bug caused by an unintended Day.js locale change in a mini‑program, explains why the global locale must remain unique, and details a comprehensive static‑analysis solution using ESLint, custom Webpack loaders, and Babel plugins to intercept and block Day.js locale modifications at compile time.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
Preventing Day.js Locale Pollution in Large Frontend Projects with Static Analysis

Preface – The Root Cause

On a quiet Friday afternoon a product alert revealed a date shift: February 22, which should have been a Thursday, appeared as Wednesday. The issue stemmed from the Day.js library handling dates in the project.

Problem Background

All date handling in the project uses the

dayjs

library, whose default locale is

en

. The locale configuration is the standard way to achieve internationalization (i18n) by adapting language, format, and cultural settings.

Localization enables language support, format adjustment (e.g., date formats), and cultural adaptation, improving global user experience.

Calling

dayjs.locale

sets a global locale. In the calendar component the code obtains the week start with

dayjs().startOf('week')

and generates the seven days by looping six times with

add(1, "day")

. The bug appeared because the first day returned was Monday instead of Sunday, indicating that the locale had been changed.

Who Modified the Locale?

The change originated from another business line that altered the Day.js locale in a sub‑package. Because the mini‑program uses sub‑packages to reduce bundle size, the configuration file was unintentionally included in the main bundle, affecting the entire app.

Mini‑Program Sub‑Package Logic

Sub‑packages (

subPackages

) load non‑critical modules on demand. However, the calendar component imported a configuration file from a sub‑package, causing the global

dayjs.locale

call to be bundled with the main package.

Is the Problem Truly Resolved?

The immediate fix was to extract the configuration into a separate file, preventing unrelated code from being bundled. However, similar issues could recur when other sub‑packages modify the locale.

Requirement Analysis

Goal

Ensure the Day.js locale configuration is globally unique across the project to avoid accidental pollution.

Solution Approach

Split the work into governance of existing code and control of new code .

Engineering Governance

Search for all

dayjs.locale()

calls, assess impact, and remove them. The team decided to keep the default

en

locale rather than switching to

zh‑cn

, which would require extensive changes.

Standardized Control

Implement compile‑time interception to forbid calls that modify the global locale. This requires static analysis of both project source files and third‑party packages in

node_modules

.

Code Design and Implementation

Technical Selection

Static Analysis Principle

Static analysis examines code without execution, using tools such as ESLint, Webpack, and Babel. It can be performed via simple string matching or more precise AST (Abstract Syntax Tree) traversal.

ESLint

Using ESLint’s

no-restricted-syntax

rule to block

dayjs.locale

calls:

<code>module.exports = {
  rules: {
    'no-restricted-syntax': ['error', {
      selector: "CallExpression[callee.object.name='dayjs'][callee.property.name='locale']",
      message: 'Calling dayjs.locale is prohibited'
    }]
  }
};
</code>

For more robust detection, a custom ESLint plugin records imported identifiers and checks member expressions.

<code>module.exports = {
  rules: {
    'no-dayjs-locale': {
      create(context) {
        const dayjsIds = new Set();
        return {
          ImportDeclaration(node) {
            if (node.source.value === 'dayjs') {
              node.specifiers.forEach(s => dayjsIds.add(s.local.name));
            }
          },
          MemberExpression(node) {
            if (dayjsIds.has(node.object.name) && node.property.name === 'locale') {
              context.report({ node, message: 'dayjs.locale is not allowed' });
            }
          }
        };
      }
    }
  }
};
</code>

Webpack Loader

A custom loader can scan source strings for

dayjs.locale(

and emit a compilation error:

<code>module.exports = function(source) {
  if (source.includes('dayjs.locale(')) {
    this.emitError(new Error(`Forbidden call to dayjs.locale() in ${this.resourcePath}`));
  }
  return source;
};
</code>

However, simple string matching fails when the import is renamed, so an AST‑based loader is preferred.

AST‑Based Loader (Acorn + Estraverse)

<code>const acorn = require('acorn');
const estraverse = require('estraverse');
module.exports = function(source) {
  const ast = acorn.parse(source, { ecmaVersion: 2020, sourceType: 'module' });
  estraverse.replace(ast, {
    enter(node) {
      if (node.type === 'CallExpression' &&
          node.callee.type === 'MemberExpression' &&
          node.callee.object.name === 'dayjs' &&
          node.callee.property.name === 'locale') {
        const arg = node.arguments[0];
        if (arg && arg.type === 'Literal' && arg.value === 'fr') {
          arg.value = 'en'; // force default locale
        }
      }
    }
  });
  const escodegen = require('escodegen');
  return escodegen.generate(ast);
};
</code>

Babel Plugin

A Babel plugin tracks imported identifiers and rewrites or blocks

.locale()

calls:

<code>module.exports = function({ types: t }) {
  let dayjsId = null;
  return {
    visitor: {
      ImportDeclaration(path) {
        if (path.node.source.value === 'dayjs') {
          const spec = path.node.specifiers[0];
          if (t.isImportDefaultSpecifier(spec)) dayjsId = spec.local.name;
        }
      },
      MemberExpression(path) {
        if (dayjsId && t.isIdentifier(path.node.object, { name: dayjsId }) &&
            t.isIdentifier(path.node.property, { name: 'locale' })) {
          const call = path.parentPath;
          if (call.isCallExpression()) {
            const args = call.node.arguments;
            if (args.length && t.isStringLiteral(args[0])) {
              args[0] = t.stringLiteral('en'); // enforce default
            }
          }
        }
      }
    }
  };
};
</code>

Generic Ban‑Usage Plugin

The team later adopted a configurable plugin that can forbid specific imports or method calls (e.g.,

dayjs.default.extend

,

dayjs.default.locale

) across both source code and third‑party packages.

<code>module.exports = {
  plugins: [
    ["@guming/babel-plugin-ban-usage", {
      rules: [
        { source: "dayjs", ban: ["default.extend", "default.locale"] },
        { source: "dayjs/plugin/xxx", import: false }
      ]
    }]
  ]
};
</code>

Summary

The article demonstrates how the team identified a subtle Day.js locale mutation that broke date calculations, and how they established a robust static‑analysis pipeline—leveraging ESLint, custom Webpack loaders, and Babel plugins—to enforce a single global locale configuration and prevent future accidental modifications.

FrontendBabelWebpackstatic analysisESLintdayjslocale
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.