diff --git a/Taskfile.yml b/Taskfile.yml
index 095cae2dd87..6dac2c66d54 100755
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -309,24 +309,24 @@ tasks:
   i18n:update:
     desc: Updates i18n files
     cmds:
-      - go run ./internal/i18n/cmd/main.go catalog generate . > ./internal/i18n/data/en.po
+      - go run ./internal/locales/cmd/main.go catalog generate . > ./internal/locales/data/en.po
 
   i18n:pull:
     desc: Pull i18n files from transifex
     cmds:
-      - go run ./internal/i18n/cmd/main.go transifex pull ./internal/i18n/data
+      - go run ./internal/locales/cmd/main.go transifex pull ./internal/locales/data
 
   i18n:push:
     desc: Push i18n files to transifex
     cmds:
-      - go run ./internal/i18n/cmd/main.go transifex push ./internal/i18n/data
+      - go run ./internal/locales/cmd/main.go transifex push ./internal/locales/data
 
   i18n:check:
     desc: Check if the i18n message catalog was updated
     cmds:
       - task: i18n:pull
-      - git add -N ./internal/i18n/data
-      - git diff --exit-code ./internal/i18n/data
+      - git add -N ./internal/locales/data
+      - git diff --exit-code ./internal/locales/data
 
   # Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/check-mkdocs-task/Taskfile.yml
   website:check:
diff --git a/commands/instances.go b/commands/instances.go
index 23f42be9638..3c023301cd4 100644
--- a/commands/instances.go
+++ b/commands/instances.go
@@ -38,6 +38,7 @@ import (
 	"github.com/arduino/arduino-cli/internal/arduino/sketch"
 	"github.com/arduino/arduino-cli/internal/arduino/utils"
 	"github.com/arduino/arduino-cli/internal/i18n"
+	"github.com/arduino/arduino-cli/internal/locales"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	paths "github.com/arduino/go-paths-helper"
 	"github.com/sirupsen/logrus"
@@ -420,7 +421,7 @@ func (s *arduinoCoreServerImpl) Init(req *rpc.InitRequest, stream rpc.ArduinoCor
 	// language of the CLI if the locale is different
 	// after started.
 	if locale, ok, _ := s.settings.GetStringOk("locale"); ok {
-		i18n.Init(locale)
+		locales.Init(locale)
 	}
 
 	return nil
diff --git a/commands/service_board_details.go b/commands/service_board_details.go
index 5f042452582..4104c9bc21b 100644
--- a/commands/service_board_details.go
+++ b/commands/service_board_details.go
@@ -20,8 +20,8 @@ import (
 
 	"github.com/arduino/arduino-cli/commands/cmderrors"
 	"github.com/arduino/arduino-cli/commands/internal/instances"
-	"github.com/arduino/arduino-cli/internal/arduino/cores"
 	"github.com/arduino/arduino-cli/internal/arduino/utils"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 )
 
@@ -34,7 +34,7 @@ func (s *arduinoCoreServerImpl) BoardDetails(ctx context.Context, req *rpc.Board
 	}
 	defer release()
 
-	fqbn, err := cores.ParseFQBN(req.GetFqbn())
+	fqbn, err := fqbn.Parse(req.GetFqbn())
 	if err != nil {
 		return nil, &cmderrors.InvalidFQBNError{Cause: err}
 	}
@@ -48,7 +48,7 @@ func (s *arduinoCoreServerImpl) BoardDetails(ctx context.Context, req *rpc.Board
 	details.Name = board.Name()
 	details.Fqbn = board.FQBN()
 	details.PropertiesId = board.BoardID
-	details.Official = fqbn.Package == "arduino"
+	details.Official = fqbn.Packager == "arduino"
 	details.Version = board.PlatformRelease.Version.String()
 	details.IdentificationProperties = []*rpc.BoardIdentificationProperties{}
 	for _, p := range board.GetIdentificationProperties() {
diff --git a/commands/service_board_list.go b/commands/service_board_list.go
index 2b124c29f37..9a84e3319f5 100644
--- a/commands/service_board_list.go
+++ b/commands/service_board_list.go
@@ -30,11 +30,11 @@ import (
 	"github.com/arduino/arduino-cli/commands/cmderrors"
 	"github.com/arduino/arduino-cli/commands/internal/instances"
 	f "github.com/arduino/arduino-cli/internal/algorithms"
-	"github.com/arduino/arduino-cli/internal/arduino/cores"
 	"github.com/arduino/arduino-cli/internal/arduino/cores/packagemanager"
 	"github.com/arduino/arduino-cli/internal/cli/configuration"
 	"github.com/arduino/arduino-cli/internal/i18n"
 	"github.com/arduino/arduino-cli/internal/inventory"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	"github.com/arduino/go-properties-orderedmap"
 	discovery "github.com/arduino/pluggable-discovery-protocol-handler/v2"
@@ -148,7 +148,7 @@ func identify(pme *packagemanager.Explorer, port *discovery.Port, settings *conf
 	// first query installed cores through the Package Manager
 	logrus.Debug("Querying installed cores for board identification...")
 	for _, board := range pme.IdentifyBoard(port.Properties) {
-		fqbn, err := cores.ParseFQBN(board.FQBN())
+		fqbn, err := fqbn.Parse(board.FQBN())
 		if err != nil {
 			return nil, &cmderrors.InvalidFQBNError{Cause: err}
 		}
@@ -210,10 +210,10 @@ func (s *arduinoCoreServerImpl) BoardList(ctx context.Context, req *rpc.BoardLis
 	}
 	defer release()
 
-	var fqbnFilter *cores.FQBN
+	var fqbnFilter *fqbn.FQBN
 	if f := req.GetFqbn(); f != "" {
 		var err error
-		fqbnFilter, err = cores.ParseFQBN(f)
+		fqbnFilter, err = fqbn.Parse(f)
 		if err != nil {
 			return nil, &cmderrors.InvalidFQBNError{Cause: err}
 		}
@@ -247,9 +247,9 @@ func (s *arduinoCoreServerImpl) BoardList(ctx context.Context, req *rpc.BoardLis
 	}, nil
 }
 
-func hasMatchingBoard(b *rpc.DetectedPort, fqbnFilter *cores.FQBN) bool {
+func hasMatchingBoard(b *rpc.DetectedPort, fqbnFilter *fqbn.FQBN) bool {
 	for _, detectedBoard := range b.GetMatchingBoards() {
-		detectedFqbn, err := cores.ParseFQBN(detectedBoard.GetFqbn())
+		detectedFqbn, err := fqbn.Parse(detectedBoard.GetFqbn())
 		if err != nil {
 			continue
 		}
diff --git a/commands/service_compile.go b/commands/service_compile.go
index a7ce1ea2bbf..61f8e1ef1d7 100644
--- a/commands/service_compile.go
+++ b/commands/service_compile.go
@@ -28,13 +28,13 @@ import (
 	"github.com/arduino/arduino-cli/commands/cmderrors"
 	"github.com/arduino/arduino-cli/commands/internal/instances"
 	"github.com/arduino/arduino-cli/internal/arduino/builder"
-	"github.com/arduino/arduino-cli/internal/arduino/cores"
 	"github.com/arduino/arduino-cli/internal/arduino/libraries/librariesmanager"
 	"github.com/arduino/arduino-cli/internal/arduino/sketch"
 	"github.com/arduino/arduino-cli/internal/arduino/utils"
 	"github.com/arduino/arduino-cli/internal/buildcache"
 	"github.com/arduino/arduino-cli/internal/i18n"
 	"github.com/arduino/arduino-cli/internal/inventory"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	paths "github.com/arduino/go-paths-helper"
 	"github.com/sirupsen/logrus"
@@ -116,7 +116,7 @@ func (s *arduinoCoreServerImpl) Compile(req *rpc.CompileRequest, stream rpc.Ardu
 		return &cmderrors.MissingFQBNError{}
 	}
 
-	fqbn, err := cores.ParseFQBN(fqbnIn)
+	fqbn, err := fqbn.Parse(fqbnIn)
 	if err != nil {
 		return &cmderrors.InvalidFQBNError{Cause: err}
 	}
@@ -124,7 +124,7 @@ func (s *arduinoCoreServerImpl) Compile(req *rpc.CompileRequest, stream rpc.Ardu
 	if err != nil {
 		if targetPlatform == nil {
 			return &cmderrors.PlatformNotFoundError{
-				Platform: fmt.Sprintf("%s:%s", fqbn.Package, fqbn.PlatformArch),
+				Platform: fmt.Sprintf("%s:%s", fqbn.Packager, fqbn.Architecture),
 				Cause:    errors.New(i18n.Tr("platform not installed")),
 			}
 		}
diff --git a/commands/service_debug_config.go b/commands/service_debug_config.go
index c2cf04e5aa3..f755b68adb5 100644
--- a/commands/service_debug_config.go
+++ b/commands/service_debug_config.go
@@ -27,10 +27,10 @@ import (
 
 	"github.com/arduino/arduino-cli/commands/cmderrors"
 	"github.com/arduino/arduino-cli/commands/internal/instances"
-	"github.com/arduino/arduino-cli/internal/arduino/cores"
 	"github.com/arduino/arduino-cli/internal/arduino/cores/packagemanager"
 	"github.com/arduino/arduino-cli/internal/arduino/sketch"
 	"github.com/arduino/arduino-cli/internal/i18n"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	"github.com/arduino/go-paths-helper"
 	"github.com/arduino/go-properties-orderedmap"
@@ -76,7 +76,7 @@ func (s *arduinoCoreServerImpl) IsDebugSupported(ctx context.Context, req *rpc.I
 
 	// Compute the minimum FQBN required to get the same debug configuration.
 	// (i.e. the FQBN cleaned up of the options that do not affect the debugger configuration)
-	minimumFQBN := cores.MustParseFQBN(req.GetFqbn())
+	minimumFQBN := fqbn.MustParse(req.GetFqbn())
 	for _, config := range minimumFQBN.Configs.Keys() {
 		checkFQBN := minimumFQBN.Clone()
 		checkFQBN.Configs.Remove(config)
@@ -127,7 +127,7 @@ func (s *arduinoCoreServerImpl) getDebugProperties(req *rpc.GetDebugConfigReques
 	if fqbnIn == "" {
 		return nil, &cmderrors.MissingFQBNError{}
 	}
-	fqbn, err := cores.ParseFQBN(fqbnIn)
+	fqbn, err := fqbn.Parse(fqbnIn)
 	if err != nil {
 		return nil, &cmderrors.InvalidFQBNError{Cause: err}
 	}
diff --git a/commands/service_library_list.go b/commands/service_library_list.go
index 35104caf08b..2d30e11dbd3 100644
--- a/commands/service_library_list.go
+++ b/commands/service_library_list.go
@@ -21,12 +21,12 @@ import (
 
 	"github.com/arduino/arduino-cli/commands/cmderrors"
 	"github.com/arduino/arduino-cli/commands/internal/instances"
-	"github.com/arduino/arduino-cli/internal/arduino/cores"
 	"github.com/arduino/arduino-cli/internal/arduino/libraries"
 	"github.com/arduino/arduino-cli/internal/arduino/libraries/librariesindex"
 	"github.com/arduino/arduino-cli/internal/arduino/libraries/librariesmanager"
 	"github.com/arduino/arduino-cli/internal/arduino/libraries/librariesresolver"
 	"github.com/arduino/arduino-cli/internal/i18n"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 )
 
@@ -59,7 +59,7 @@ func (s *arduinoCoreServerImpl) LibraryList(ctx context.Context, req *rpc.Librar
 	var allLibs []*installedLib
 	if fqbnString := req.GetFqbn(); fqbnString != "" {
 		allLibs = listLibraries(lme, li, req.GetUpdatable(), true)
-		fqbn, err := cores.ParseFQBN(req.GetFqbn())
+		fqbn, err := fqbn.Parse(req.GetFqbn())
 		if err != nil {
 			return nil, &cmderrors.InvalidFQBNError{Cause: err}
 		}
@@ -77,8 +77,8 @@ func (s *arduinoCoreServerImpl) LibraryList(ctx context.Context, req *rpc.Librar
 				}
 			}
 			if latest, has := filteredRes[lib.Library.Name]; has {
-				latestPriority := librariesresolver.ComputePriority(latest.Library, "", fqbn.PlatformArch)
-				libPriority := librariesresolver.ComputePriority(lib.Library, "", fqbn.PlatformArch)
+				latestPriority := librariesresolver.ComputePriority(latest.Library, "", fqbn.Architecture)
+				libPriority := librariesresolver.ComputePriority(lib.Library, "", fqbn.Architecture)
 				if latestPriority >= libPriority {
 					// Pick library with the best priority
 					continue
@@ -87,7 +87,7 @@ func (s *arduinoCoreServerImpl) LibraryList(ctx context.Context, req *rpc.Librar
 
 			// Check if library is compatible with board specified by FBQN
 			lib.Library.CompatibleWith = map[string]bool{
-				fqbnString: lib.Library.IsCompatibleWith(fqbn.PlatformArch),
+				fqbnString: lib.Library.IsCompatibleWith(fqbn.Architecture),
 			}
 
 			filteredRes[lib.Library.Name] = lib
diff --git a/commands/service_monitor.go b/commands/service_monitor.go
index 8c3402681b7..012d4ddf8bc 100644
--- a/commands/service_monitor.go
+++ b/commands/service_monitor.go
@@ -28,6 +28,7 @@ import (
 	"github.com/arduino/arduino-cli/internal/arduino/cores/packagemanager"
 	pluggableMonitor "github.com/arduino/arduino-cli/internal/arduino/monitor"
 	"github.com/arduino/arduino-cli/internal/i18n"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	"github.com/arduino/go-properties-orderedmap"
 	"github.com/djherbis/buffer"
@@ -237,7 +238,7 @@ func (s *arduinoCoreServerImpl) Monitor(stream rpc.ArduinoCoreService_MonitorSer
 	return nil
 }
 
-func findMonitorAndSettingsForProtocolAndBoard(pme *packagemanager.Explorer, protocol, fqbn string) (*pluggableMonitor.PluggableMonitor, *properties.Map, error) {
+func findMonitorAndSettingsForProtocolAndBoard(pme *packagemanager.Explorer, protocol, fqbnIn string) (*pluggableMonitor.PluggableMonitor, *properties.Map, error) {
 	if protocol == "" {
 		return nil, nil, &cmderrors.MissingPortProtocolError{}
 	}
@@ -246,8 +247,8 @@ func findMonitorAndSettingsForProtocolAndBoard(pme *packagemanager.Explorer, pro
 	boardSettings := properties.NewMap()
 
 	// If a board is specified search the monitor in the board package first
-	if fqbn != "" {
-		fqbn, err := cores.ParseFQBN(fqbn)
+	if fqbnIn != "" {
+		fqbn, err := fqbn.Parse(fqbnIn)
 		if err != nil {
 			return nil, nil, &cmderrors.InvalidFQBNError{Cause: err}
 		}
diff --git a/commands/service_upload.go b/commands/service_upload.go
index 56d621813fe..2e5e9272d51 100644
--- a/commands/service_upload.go
+++ b/commands/service_upload.go
@@ -32,6 +32,7 @@ import (
 	"github.com/arduino/arduino-cli/internal/arduino/globals"
 	"github.com/arduino/arduino-cli/internal/arduino/sketch"
 	"github.com/arduino/arduino-cli/internal/i18n"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	paths "github.com/arduino/go-paths-helper"
 	properties "github.com/arduino/go-properties-orderedmap"
@@ -53,7 +54,7 @@ func (s *arduinoCoreServerImpl) SupportedUserFields(ctx context.Context, req *rp
 	}
 	defer release()
 
-	fqbn, err := cores.ParseFQBN(req.GetFqbn())
+	fqbn, err := fqbn.Parse(req.GetFqbn())
 	if err != nil {
 		return nil, &cmderrors.InvalidFQBNError{Cause: err}
 	}
@@ -61,7 +62,7 @@ func (s *arduinoCoreServerImpl) SupportedUserFields(ctx context.Context, req *rp
 	_, platformRelease, _, boardProperties, _, err := pme.ResolveFQBN(fqbn)
 	if platformRelease == nil {
 		return nil, &cmderrors.PlatformNotFoundError{
-			Platform: fmt.Sprintf("%s:%s", fqbn.Package, fqbn.PlatformArch),
+			Platform: fmt.Sprintf("%s:%s", fqbn.Packager, fqbn.Architecture),
 			Cause:    err,
 		}
 	} else if err != nil {
@@ -282,7 +283,7 @@ func (s *arduinoCoreServerImpl) runProgramAction(ctx context.Context, pme *packa
 		return nil, &cmderrors.MissingProgrammerError{}
 	}
 
-	fqbn, err := cores.ParseFQBN(fqbnIn)
+	fqbn, err := fqbn.Parse(fqbnIn)
 	if err != nil {
 		return nil, &cmderrors.InvalidFQBNError{Cause: err}
 	}
@@ -292,7 +293,7 @@ func (s *arduinoCoreServerImpl) runProgramAction(ctx context.Context, pme *packa
 	_, boardPlatform, board, boardProperties, buildPlatform, err := pme.ResolveFQBN(fqbn)
 	if boardPlatform == nil {
 		return nil, &cmderrors.PlatformNotFoundError{
-			Platform: fmt.Sprintf("%s:%s", fqbn.Package, fqbn.PlatformArch),
+			Platform: fmt.Sprintf("%s:%s", fqbn.Packager, fqbn.Architecture),
 			Cause:    err,
 		}
 	} else if err != nil {
diff --git a/commands/service_upload_list_programmers.go b/commands/service_upload_list_programmers.go
index f05142cf147..761d9babf5c 100644
--- a/commands/service_upload_list_programmers.go
+++ b/commands/service_upload_list_programmers.go
@@ -21,6 +21,7 @@ import (
 	"github.com/arduino/arduino-cli/commands/cmderrors"
 	"github.com/arduino/arduino-cli/commands/internal/instances"
 	"github.com/arduino/arduino-cli/internal/arduino/cores"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 )
 
@@ -36,7 +37,7 @@ func (s *arduinoCoreServerImpl) ListProgrammersAvailableForUpload(ctx context.Co
 	if fqbnIn == "" {
 		return nil, &cmderrors.MissingFQBNError{}
 	}
-	fqbn, err := cores.ParseFQBN(fqbnIn)
+	fqbn, err := fqbn.Parse(fqbnIn)
 	if err != nil {
 		return nil, &cmderrors.InvalidFQBNError{Cause: err}
 	}
diff --git a/commands/service_upload_test.go b/commands/service_upload_test.go
index 737eec92e83..4a86a0f274b 100644
--- a/commands/service_upload_test.go
+++ b/commands/service_upload_test.go
@@ -25,6 +25,7 @@ import (
 	"github.com/arduino/arduino-cli/internal/arduino/cores"
 	"github.com/arduino/arduino-cli/internal/arduino/cores/packagemanager"
 	"github.com/arduino/arduino-cli/internal/arduino/sketch"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	paths "github.com/arduino/go-paths-helper"
 	properties "github.com/arduino/go-properties-orderedmap"
@@ -60,7 +61,7 @@ func TestDetermineBuildPathAndSketchName(t *testing.T) {
 		importFile    string
 		importDir     string
 		sketch        *sketch.Sketch
-		fqbn          *cores.FQBN
+		fqbn          *fqbn.FQBN
 		resBuildPath  string
 		resSketchName string
 	}
@@ -68,7 +69,7 @@ func TestDetermineBuildPathAndSketchName(t *testing.T) {
 	blonk, err := sketch.New(paths.New("testdata/upload/Blonk"))
 	require.NoError(t, err)
 
-	fqbn, err := cores.ParseFQBN("arduino:samd:mkr1000")
+	fqbn, err := fqbn.Parse("arduino:samd:mkr1000")
 	require.NoError(t, err)
 
 	srv := NewArduinoCoreServer().(*arduinoCoreServerImpl)
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 3d801613a01..7f615230da6 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -303,7 +303,7 @@ package main
 
 import (
   "fmt"
-  "github.com/arduino/arduino-cli/i18n"
+  "github.com/arduino/arduino-cli/internal/i18n"
 )
 
 func main() {
diff --git a/internal/arduino/builder/build_options_manager.go b/internal/arduino/builder/build_options_manager.go
index 09f0afad815..ebaf97bda28 100644
--- a/internal/arduino/builder/build_options_manager.go
+++ b/internal/arduino/builder/build_options_manager.go
@@ -22,9 +22,9 @@ import (
 	"strings"
 
 	"github.com/arduino/arduino-cli/internal/arduino/builder/internal/utils"
-	"github.com/arduino/arduino-cli/internal/arduino/cores"
 	"github.com/arduino/arduino-cli/internal/arduino/sketch"
 	"github.com/arduino/arduino-cli/internal/i18n"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	"github.com/arduino/go-paths-helper"
 	properties "github.com/arduino/go-properties-orderedmap"
 )
@@ -51,7 +51,7 @@ func newBuildOptions(
 	builtInLibrariesDirs, buildPath *paths.Path,
 	sketch *sketch.Sketch,
 	customBuildProperties []string,
-	fqbn *cores.FQBN,
+	fqbn *fqbn.FQBN,
 	clean bool,
 	compilerOptimizationFlags string,
 	runtimePlatformPath, buildCorePath *paths.Path,
diff --git a/internal/arduino/builder/builder.go b/internal/arduino/builder/builder.go
index 91c74965782..58d607c827a 100644
--- a/internal/arduino/builder/builder.go
+++ b/internal/arduino/builder/builder.go
@@ -35,6 +35,7 @@ import (
 	"github.com/arduino/arduino-cli/internal/arduino/libraries/librariesmanager"
 	"github.com/arduino/arduino-cli/internal/arduino/sketch"
 	"github.com/arduino/arduino-cli/internal/i18n"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	"github.com/arduino/go-paths-helper"
 	"github.com/arduino/go-properties-orderedmap"
@@ -127,7 +128,7 @@ func NewBuilder(
 	requestBuildProperties []string,
 	hardwareDirs, otherLibrariesDirs paths.PathList,
 	builtInLibrariesDirs *paths.Path,
-	fqbn *cores.FQBN,
+	fqbn *fqbn.FQBN,
 	clean bool,
 	sourceOverrides map[string]string,
 	onlyUpdateCompilationDatabase bool,
diff --git a/internal/arduino/cores/board.go b/internal/arduino/cores/board.go
index ed1aa9c68b5..6e966a68aee 100644
--- a/internal/arduino/cores/board.go
+++ b/internal/arduino/cores/board.go
@@ -21,6 +21,7 @@ import (
 	"sync"
 
 	"github.com/arduino/arduino-cli/internal/i18n"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	"github.com/arduino/go-properties-orderedmap"
 )
 
@@ -124,7 +125,7 @@ func (b *Board) GetConfigOptionValues(option string) *properties.Map {
 
 // GetBuildProperties returns the build properties and the build
 // platform for the Board with the configuration passed as parameter.
-func (b *Board) GetBuildProperties(fqbn *FQBN) (*properties.Map, error) {
+func (b *Board) GetBuildProperties(fqbn *fqbn.FQBN) (*properties.Map, error) {
 	b.buildConfigOptionsStructures()
 
 	// Override default configs with user configs
@@ -161,7 +162,7 @@ func (b *Board) GetBuildProperties(fqbn *FQBN) (*properties.Map, error) {
 // "cpu=atmega2560".
 // FIXME: deprecated, use GetBuildProperties instead
 func (b *Board) GeneratePropertiesForConfiguration(config string) (*properties.Map, error) {
-	fqbn, err := ParseFQBN(b.String() + ":" + config)
+	fqbn, err := fqbn.Parse(b.String() + ":" + config)
 	if err != nil {
 		return nil, errors.New(i18n.Tr("parsing fqbn: %s", err))
 	}
diff --git a/internal/arduino/cores/packagemanager/package_manager.go b/internal/arduino/cores/packagemanager/package_manager.go
index aaaf675a410..4c22c19c6e4 100644
--- a/internal/arduino/cores/packagemanager/package_manager.go
+++ b/internal/arduino/cores/packagemanager/package_manager.go
@@ -33,6 +33,7 @@ import (
 	"github.com/arduino/arduino-cli/internal/arduino/discovery/discoverymanager"
 	"github.com/arduino/arduino-cli/internal/arduino/sketch"
 	"github.com/arduino/arduino-cli/internal/i18n"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	paths "github.com/arduino/go-paths-helper"
 	properties "github.com/arduino/go-properties-orderedmap"
 	"github.com/arduino/go-timeutils"
@@ -290,7 +291,7 @@ func (pme *Explorer) FindBoardsWithID(id string) []*cores.Board {
 
 // FindBoardWithFQBN returns the board identified by the fqbn, or an error
 func (pme *Explorer) FindBoardWithFQBN(fqbnIn string) (*cores.Board, error) {
-	fqbn, err := cores.ParseFQBN(fqbnIn)
+	fqbn, err := fqbn.Parse(fqbnIn)
 	if err != nil {
 		return nil, errors.New(i18n.Tr("parsing fqbn: %s", err))
 	}
@@ -318,22 +319,22 @@ func (pme *Explorer) FindBoardWithFQBN(fqbnIn string) (*cores.Board, error) {
 //
 // In case of error the partial results found in the meantime are
 // returned together with the error.
-func (pme *Explorer) ResolveFQBN(fqbn *cores.FQBN) (
+func (pme *Explorer) ResolveFQBN(fqbn *fqbn.FQBN) (
 	*cores.Package, *cores.PlatformRelease, *cores.Board,
 	*properties.Map, *cores.PlatformRelease, error) {
 
 	// Find package
-	targetPackage := pme.packages[fqbn.Package]
+	targetPackage := pme.packages[fqbn.Packager]
 	if targetPackage == nil {
 		return nil, nil, nil, nil, nil,
-			errors.New(i18n.Tr("unknown package %s", fqbn.Package))
+			errors.New(i18n.Tr("unknown package %s", fqbn.Packager))
 	}
 
 	// Find platform
-	platform := targetPackage.Platforms[fqbn.PlatformArch]
+	platform := targetPackage.Platforms[fqbn.Architecture]
 	if platform == nil {
 		return targetPackage, nil, nil, nil, nil,
-			errors.New(i18n.Tr("unknown platform %s:%s", targetPackage, fqbn.PlatformArch))
+			errors.New(i18n.Tr("unknown platform %s:%s", targetPackage, fqbn.Architecture))
 	}
 	boardPlatformRelease := pme.GetInstalledPlatformRelease(platform)
 	if boardPlatformRelease == nil {
@@ -429,7 +430,7 @@ func (pme *Explorer) ResolveFQBN(fqbn *cores.FQBN) (
 	return targetPackage, boardPlatformRelease, board, buildProperties, corePlatformRelease, nil
 }
 
-func (pme *Explorer) determineReferencedPlatformRelease(boardBuildProperties *properties.Map, boardPlatformRelease *cores.PlatformRelease, fqbn *cores.FQBN) (string, *cores.PlatformRelease, string, *cores.PlatformRelease, error) {
+func (pme *Explorer) determineReferencedPlatformRelease(boardBuildProperties *properties.Map, boardPlatformRelease *cores.PlatformRelease, fqbn *fqbn.FQBN) (string, *cores.PlatformRelease, string, *cores.PlatformRelease, error) {
 	core := boardBuildProperties.ExpandPropsInString(boardBuildProperties.Get("build.core"))
 	referredCore := ""
 	if split := strings.Split(core, ":"); len(split) > 1 {
@@ -461,15 +462,15 @@ func (pme *Explorer) determineReferencedPlatformRelease(boardBuildProperties *pr
 			return "", nil, "", nil,
 				errors.New(i18n.Tr("missing package %[1]s referenced by board %[2]s", referredPackageName, fqbn))
 		}
-		referredPlatform := referredPackage.Platforms[fqbn.PlatformArch]
+		referredPlatform := referredPackage.Platforms[fqbn.Architecture]
 		if referredPlatform == nil {
 			return "", nil, "", nil,
-				errors.New(i18n.Tr("missing platform %[1]s:%[2]s referenced by board %[3]s", referredPackageName, fqbn.PlatformArch, fqbn))
+				errors.New(i18n.Tr("missing platform %[1]s:%[2]s referenced by board %[3]s", referredPackageName, fqbn.Architecture, fqbn))
 		}
 		referredPlatformRelease = pme.GetInstalledPlatformRelease(referredPlatform)
 		if referredPlatformRelease == nil {
 			return "", nil, "", nil,
-				errors.New(i18n.Tr("missing platform release %[1]s:%[2]s referenced by board %[3]s", referredPackageName, fqbn.PlatformArch, fqbn))
+				errors.New(i18n.Tr("missing platform release %[1]s:%[2]s referenced by board %[3]s", referredPackageName, fqbn.Architecture, fqbn))
 		}
 	}
 
@@ -890,7 +891,7 @@ func (pme *Explorer) FindMonitorDependency(discovery *cores.MonitorDependency) *
 
 // NormalizeFQBN return a normalized copy of the given FQBN, that is the same
 // FQBN but with the unneeded or invalid options removed.
-func (pme *Explorer) NormalizeFQBN(fqbn *cores.FQBN) (*cores.FQBN, error) {
+func (pme *Explorer) NormalizeFQBN(fqbn *fqbn.FQBN) (*fqbn.FQBN, error) {
 	_, _, board, _, _, err := pme.ResolveFQBN(fqbn)
 	if err != nil {
 		return nil, err
diff --git a/internal/arduino/cores/packagemanager/package_manager_test.go b/internal/arduino/cores/packagemanager/package_manager_test.go
index 4f8598a515a..92530af6dc3 100644
--- a/internal/arduino/cores/packagemanager/package_manager_test.go
+++ b/internal/arduino/cores/packagemanager/package_manager_test.go
@@ -24,6 +24,7 @@ import (
 	"testing"
 
 	"github.com/arduino/arduino-cli/internal/arduino/cores"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	"github.com/arduino/go-paths-helper"
 	"github.com/arduino/go-properties-orderedmap"
 	"github.com/stretchr/testify/require"
@@ -69,7 +70,7 @@ func TestResolveFQBN(t *testing.T) {
 
 	t.Run("NormalizeFQBN", func(t *testing.T) {
 		testNormalization := func(in, expected string) {
-			fqbn, err := cores.ParseFQBN(in)
+			fqbn, err := fqbn.Parse(in)
 			require.Nil(t, err)
 			require.NotNil(t, fqbn)
 			normalized, err := pme.NormalizeFQBN(fqbn)
@@ -92,7 +93,7 @@ func TestResolveFQBN(t *testing.T) {
 	})
 
 	t.Run("BoardAndBuildPropertiesArduinoUno", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("arduino:avr:uno")
+		fqbn, err := fqbn.Parse("arduino:avr:uno")
 		require.Nil(t, err)
 		require.NotNil(t, fqbn)
 		pkg, platformRelease, board, props, buildPlatformRelease, err := pme.ResolveFQBN(fqbn)
@@ -113,7 +114,7 @@ func TestResolveFQBN(t *testing.T) {
 	})
 
 	t.Run("BoardAndBuildPropertiesArduinoMega", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("arduino:avr:mega")
+		fqbn, err := fqbn.Parse("arduino:avr:mega")
 		require.Nil(t, err)
 		require.NotNil(t, fqbn)
 		pkg, platformRelease, board, props, buildPlatformRelease, err := pme.ResolveFQBN(fqbn)
@@ -129,7 +130,7 @@ func TestResolveFQBN(t *testing.T) {
 	})
 
 	t.Run("BoardAndBuildPropertiesArduinoMegaWithNonDefaultCpuOption", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("arduino:avr:mega:cpu=atmega1280")
+		fqbn, err := fqbn.Parse("arduino:avr:mega:cpu=atmega1280")
 		require.Nil(t, err)
 		require.NotNil(t, fqbn)
 		pkg, platformRelease, board, props, buildPlatformRelease, err := pme.ResolveFQBN(fqbn)
@@ -147,7 +148,7 @@ func TestResolveFQBN(t *testing.T) {
 	})
 
 	t.Run("BoardAndBuildPropertiesArduinoMegaWithDefaultCpuOption", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("arduino:avr:mega:cpu=atmega2560")
+		fqbn, err := fqbn.Parse("arduino:avr:mega:cpu=atmega2560")
 		require.Nil(t, err)
 		require.NotNil(t, fqbn)
 		pkg, platformRelease, board, props, buildPlatformRelease, err := pme.ResolveFQBN(fqbn)
@@ -167,7 +168,7 @@ func TestResolveFQBN(t *testing.T) {
 
 	t.Run("BoardAndBuildPropertiesForReferencedArduinoUno", func(t *testing.T) {
 		// Test a board referenced from the main AVR arduino platform
-		fqbn, err := cores.ParseFQBN("referenced:avr:uno")
+		fqbn, err := fqbn.Parse("referenced:avr:uno")
 		require.Nil(t, err)
 		require.NotNil(t, fqbn)
 		pkg, platformRelease, board, props, buildPlatformRelease, err := pme.ResolveFQBN(fqbn)
@@ -185,7 +186,7 @@ func TestResolveFQBN(t *testing.T) {
 	})
 
 	t.Run("BoardAndBuildPropertiesForArduinoDue", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("arduino:sam:arduino_due_x")
+		fqbn, err := fqbn.Parse("arduino:sam:arduino_due_x")
 		require.Nil(t, err)
 		require.NotNil(t, fqbn)
 		pkg, platformRelease, board, props, buildPlatformRelease, err := pme.ResolveFQBN(fqbn)
@@ -200,7 +201,7 @@ func TestResolveFQBN(t *testing.T) {
 	})
 
 	t.Run("BoardAndBuildPropertiesForCustomArduinoYun", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("my_avr_platform:avr:custom_yun")
+		fqbn, err := fqbn.Parse("my_avr_platform:avr:custom_yun")
 		require.Nil(t, err)
 		require.NotNil(t, fqbn)
 		pkg, platformRelease, board, props, buildPlatformRelease, err := pme.ResolveFQBN(fqbn)
@@ -216,7 +217,7 @@ func TestResolveFQBN(t *testing.T) {
 	})
 
 	t.Run("BoardAndBuildPropertiesForWatterotCore", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("watterott:avr:attiny841:core=spencekonde,info=info")
+		fqbn, err := fqbn.Parse("watterott:avr:attiny841:core=spencekonde,info=info")
 		require.Nil(t, err)
 		require.NotNil(t, fqbn)
 		pkg, platformRelease, board, props, buildPlatformRelease, err := pme.ResolveFQBN(fqbn)
@@ -234,7 +235,7 @@ func TestResolveFQBN(t *testing.T) {
 	t.Run("BoardAndBuildPropertiesForReferencedFeatherM0", func(t *testing.T) {
 		// Test a board referenced from the Adafruit SAMD core (this tests
 		// deriving where the package and core name are different)
-		fqbn, err := cores.ParseFQBN("referenced:samd:feather_m0")
+		fqbn, err := fqbn.Parse("referenced:samd:feather_m0")
 		require.Nil(t, err)
 		require.NotNil(t, fqbn)
 		pkg, platformRelease, board, props, buildPlatformRelease, err := pme.ResolveFQBN(fqbn)
@@ -253,7 +254,7 @@ func TestResolveFQBN(t *testing.T) {
 
 	t.Run("BoardAndBuildPropertiesForNonExistentPackage", func(t *testing.T) {
 		// Test a board referenced from a non-existent package
-		fqbn, err := cores.ParseFQBN("referenced:avr:dummy_invalid_package")
+		fqbn, err := fqbn.Parse("referenced:avr:dummy_invalid_package")
 		require.Nil(t, err)
 		require.NotNil(t, fqbn)
 		pkg, platformRelease, board, props, buildPlatformRelease, err := pme.ResolveFQBN(fqbn)
@@ -270,7 +271,7 @@ func TestResolveFQBN(t *testing.T) {
 
 	t.Run("BoardAndBuildPropertiesForNonExistentArchitecture", func(t *testing.T) {
 		// Test a board referenced from a non-existent platform/architecture
-		fqbn, err := cores.ParseFQBN("referenced:avr:dummy_invalid_platform")
+		fqbn, err := fqbn.Parse("referenced:avr:dummy_invalid_platform")
 		require.Nil(t, err)
 		require.NotNil(t, fqbn)
 		pkg, platformRelease, board, props, buildPlatformRelease, err := pme.ResolveFQBN(fqbn)
@@ -288,7 +289,7 @@ func TestResolveFQBN(t *testing.T) {
 	t.Run("BoardAndBuildPropertiesForNonExistentCore", func(t *testing.T) {
 		// Test a board referenced from a non-existent core
 		// Note that ResolveFQBN does not actually check this currently
-		fqbn, err := cores.ParseFQBN("referenced:avr:dummy_invalid_core")
+		fqbn, err := fqbn.Parse("referenced:avr:dummy_invalid_core")
 		require.Nil(t, err)
 		require.NotNil(t, fqbn)
 		pkg, platformRelease, board, props, buildPlatformRelease, err := pme.ResolveFQBN(fqbn)
@@ -306,7 +307,7 @@ func TestResolveFQBN(t *testing.T) {
 	})
 
 	t.Run("AddBuildBoardPropertyIfMissing", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("my_avr_platform:avr:mymega")
+		fqbn, err := fqbn.Parse("my_avr_platform:avr:mymega")
 		require.Nil(t, err)
 		require.NotNil(t, fqbn)
 		pkg, platformRelease, board, props, buildPlatformRelease, err := pme.ResolveFQBN(fqbn)
@@ -324,7 +325,7 @@ func TestResolveFQBN(t *testing.T) {
 	})
 
 	t.Run("AddBuildBoardPropertyIfNotMissing", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("my_avr_platform:avr:mymega:cpu=atmega1280")
+		fqbn, err := fqbn.Parse("my_avr_platform:avr:mymega:cpu=atmega1280")
 		require.Nil(t, err)
 		require.NotNil(t, fqbn)
 		pkg, platformRelease, board, props, buildPlatformRelease, err := pme.ResolveFQBN(fqbn)
@@ -813,7 +814,7 @@ func TestLegacyPackageConversionToPluggableDiscovery(t *testing.T) {
 	defer release()
 
 	{
-		fqbn, err := cores.ParseFQBN("esp32:esp32:esp32")
+		fqbn, err := fqbn.Parse("esp32:esp32:esp32")
 		require.NoError(t, err)
 		require.NotNil(t, fqbn)
 		_, platformRelease, board, _, _, err := pme.ResolveFQBN(fqbn)
@@ -836,7 +837,7 @@ func TestLegacyPackageConversionToPluggableDiscovery(t *testing.T) {
 		require.Equal(t, "{network_cmd} -i \"{upload.port.address}\" -p \"{upload.port.properties.port}\" \"--auth={upload.field.password}\" -f \"{build.path}/{build.project_name}.bin\"", platformProps.Get("tools.esptool__pluggable_network.upload.pattern"))
 	}
 	{
-		fqbn, err := cores.ParseFQBN("esp8266:esp8266:generic")
+		fqbn, err := fqbn.Parse("esp8266:esp8266:generic")
 		require.NoError(t, err)
 		require.NotNil(t, fqbn)
 		_, platformRelease, board, _, _, err := pme.ResolveFQBN(fqbn)
@@ -858,7 +859,7 @@ func TestLegacyPackageConversionToPluggableDiscovery(t *testing.T) {
 		require.Equal(t, "\"{network_cmd}\" -I \"{runtime.platform.path}/tools/espota.py\" -i \"{upload.port.address}\" -p \"{upload.port.properties.port}\" \"--auth={upload.field.password}\" -f \"{build.path}/{build.project_name}.bin\"", platformProps.Get("tools.esptool__pluggable_network.upload.pattern"))
 	}
 	{
-		fqbn, err := cores.ParseFQBN("arduino:avr:uno")
+		fqbn, err := fqbn.Parse("arduino:avr:uno")
 		require.NoError(t, err)
 		require.NotNil(t, fqbn)
 		_, platformRelease, board, _, _, err := pme.ResolveFQBN(fqbn)
@@ -888,7 +889,7 @@ func TestVariantAndCoreSelection(t *testing.T) {
 
 	// build.core test suite
 	t.Run("CoreWithoutSubstitutions", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("test2:avr:test")
+		fqbn, err := fqbn.Parse("test2:avr:test")
 		require.NoError(t, err)
 		require.NotNil(t, fqbn)
 		_, _, _, buildProps, _, err := pme.ResolveFQBN(fqbn)
@@ -897,7 +898,7 @@ func TestVariantAndCoreSelection(t *testing.T) {
 		requireSameFile(buildProps.GetPath("build.core.path"), dataDir1.Join("packages", "test2", "hardware", "avr", "1.0.0", "cores", "arduino"))
 	})
 	t.Run("CoreWithSubstitutions", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("test2:avr:test2")
+		fqbn, err := fqbn.Parse("test2:avr:test2")
 		require.NoError(t, err)
 		require.NotNil(t, fqbn)
 		_, _, _, buildProps, _, err := pme.ResolveFQBN(fqbn)
@@ -907,7 +908,7 @@ func TestVariantAndCoreSelection(t *testing.T) {
 		requireSameFile(buildProps.GetPath("build.core.path"), dataDir1.Join("packages", "test2", "hardware", "avr", "1.0.0", "cores", "arduino"))
 	})
 	t.Run("CoreWithSubstitutionsAndDefaultOption", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("test2:avr:test3")
+		fqbn, err := fqbn.Parse("test2:avr:test3")
 		require.NoError(t, err)
 		require.NotNil(t, fqbn)
 		_, _, _, buildProps, _, err := pme.ResolveFQBN(fqbn)
@@ -917,7 +918,7 @@ func TestVariantAndCoreSelection(t *testing.T) {
 		requireSameFile(buildProps.GetPath("build.core.path"), dataDir1.Join("packages", "test2", "hardware", "avr", "1.0.0", "cores", "arduino"))
 	})
 	t.Run("CoreWithSubstitutionsAndNonDefaultOption", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("test2:avr:test3:core=referenced")
+		fqbn, err := fqbn.Parse("test2:avr:test3:core=referenced")
 		require.NoError(t, err)
 		require.NotNil(t, fqbn)
 		_, _, _, buildProps, _, err := pme.ResolveFQBN(fqbn)
@@ -929,7 +930,7 @@ func TestVariantAndCoreSelection(t *testing.T) {
 
 	// build.variant test suite
 	t.Run("VariantWithoutSubstitutions", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("test2:avr:test4")
+		fqbn, err := fqbn.Parse("test2:avr:test4")
 		require.NoError(t, err)
 		require.NotNil(t, fqbn)
 		_, _, _, buildProps, _, err := pme.ResolveFQBN(fqbn)
@@ -938,7 +939,7 @@ func TestVariantAndCoreSelection(t *testing.T) {
 		requireSameFile(buildProps.GetPath("build.variant.path"), dataDir1.Join("packages", "test2", "hardware", "avr", "1.0.0", "variants", "standard"))
 	})
 	t.Run("VariantWithSubstitutions", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("test2:avr:test5")
+		fqbn, err := fqbn.Parse("test2:avr:test5")
 		require.NoError(t, err)
 		require.NotNil(t, fqbn)
 		_, _, _, buildProps, _, err := pme.ResolveFQBN(fqbn)
@@ -948,7 +949,7 @@ func TestVariantAndCoreSelection(t *testing.T) {
 		requireSameFile(buildProps.GetPath("build.variant.path"), dataDir1.Join("packages", "test2", "hardware", "avr", "1.0.0", "variants", "standard"))
 	})
 	t.Run("VariantWithSubstitutionsAndDefaultOption", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("test2:avr:test6")
+		fqbn, err := fqbn.Parse("test2:avr:test6")
 		require.NoError(t, err)
 		require.NotNil(t, fqbn)
 		_, _, _, buildProps, _, err := pme.ResolveFQBN(fqbn)
@@ -958,7 +959,7 @@ func TestVariantAndCoreSelection(t *testing.T) {
 		requireSameFile(buildProps.GetPath("build.variant.path"), dataDir1.Join("packages", "test2", "hardware", "avr", "1.0.0", "variants", "standard"))
 	})
 	t.Run("VariantWithSubstitutionsAndNonDefaultOption", func(t *testing.T) {
-		fqbn, err := cores.ParseFQBN("test2:avr:test6:variant=referenced")
+		fqbn, err := fqbn.Parse("test2:avr:test6:variant=referenced")
 		require.NoError(t, err)
 		require.NotNil(t, fqbn)
 		_, _, _, buildProps, _, err := pme.ResolveFQBN(fqbn)
diff --git a/internal/cli/board/list.go b/internal/cli/board/list.go
index 7d0c0c69082..9bd6fc37f22 100644
--- a/internal/cli/board/list.go
+++ b/internal/cli/board/list.go
@@ -24,13 +24,13 @@ import (
 
 	"github.com/arduino/arduino-cli/commands"
 	"github.com/arduino/arduino-cli/commands/cmderrors"
-	"github.com/arduino/arduino-cli/internal/arduino/cores"
 	"github.com/arduino/arduino-cli/internal/cli/arguments"
 	"github.com/arduino/arduino-cli/internal/cli/feedback"
 	"github.com/arduino/arduino-cli/internal/cli/feedback/result"
 	"github.com/arduino/arduino-cli/internal/cli/feedback/table"
 	"github.com/arduino/arduino-cli/internal/cli/instance"
 	"github.com/arduino/arduino-cli/internal/i18n"
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -159,9 +159,9 @@ func (dr listResult) String() string {
 				// to improve the user experience, show on a dedicated column
 				// the name of the core supporting the board detected
 				var coreName = ""
-				fqbn, err := cores.ParseFQBN(b.Fqbn)
+				fqbn, err := fqbn.Parse(b.Fqbn)
 				if err == nil {
-					coreName = fmt.Sprintf("%s:%s", fqbn.Package, fqbn.PlatformArch)
+					coreName = fmt.Sprintf("%s:%s", fqbn.Packager, fqbn.Architecture)
 				}
 
 				t.AddRow(address, protocol, protocolLabel, board, fqbn, coreName)
@@ -215,9 +215,9 @@ func (dr watchEventResult) String() string {
 			// to improve the user experience, show on a dedicated column
 			// the name of the core supporting the board detected
 			var coreName = ""
-			fqbn, err := cores.ParseFQBN(b.Fqbn)
+			fqbn, err := fqbn.Parse(b.Fqbn)
 			if err == nil {
-				coreName = fmt.Sprintf("%s:%s", fqbn.Package, fqbn.PlatformArch)
+				coreName = fmt.Sprintf("%s:%s", fqbn.Packager, fqbn.Architecture)
 			}
 
 			t.AddRow(address, protocol, event, board, fqbn, coreName)
diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go
index aa4f82edd33..f1941297b00 100644
--- a/internal/i18n/i18n.go
+++ b/internal/i18n/i18n.go
@@ -15,31 +15,28 @@
 
 package i18n
 
-// Init initializes the i18n module, setting the locale according to this order of preference:
-// 1. Locale specified via the function call
-// 2. OS Locale
-// 3. en (default)
-func Init(configLocale string) {
-	locales := supportedLocales()
-	if configLocale != "" {
-		if locale := findMatchingLocale(configLocale, locales); locale != "" {
-			setLocale(locale)
-			return
-		}
-	}
-
-	if osLocale := getLocaleIdentifierFromOS(); osLocale != "" {
-		if locale := findMatchingLocale(osLocale, locales); locale != "" {
-			setLocale(locale)
-			return
-		}
-	}
-
-	setLocale("en")
+import "fmt"
+
+type Locale interface {
+	Get(msg string, args ...interface{}) string
+}
+
+type nullLocale struct{}
+
+func (n nullLocale) Parse([]byte) {}
+
+func (n nullLocale) Get(msg string, args ...interface{}) string {
+	return fmt.Sprintf(msg, args...)
+}
+
+var locale Locale = &nullLocale{}
+
+func SetLocale(l Locale) {
+	locale = l
 }
 
 // Tr returns msg translated to the selected locale
 // the msg argument must be a literal string
 func Tr(msg string, args ...interface{}) string {
-	return po.Get(msg, args...)
+	return locale.Get(msg, args...)
 }
diff --git a/internal/i18n/i18n_test.go b/internal/i18n/i18n_test.go
index 1ec157db01e..a2ae4c4d7b0 100644
--- a/internal/i18n/i18n_test.go
+++ b/internal/i18n/i18n_test.go
@@ -25,8 +25,9 @@ import (
 )
 
 func setPo(poFile string) {
-	po = gotext.NewPo()
-	po.Parse([]byte(poFile))
+	dict := gotext.NewPo()
+	dict.Parse([]byte(poFile))
+	SetLocale(dict)
 }
 
 func TestPoTranslation(t *testing.T) {
@@ -39,7 +40,7 @@ func TestPoTranslation(t *testing.T) {
 }
 
 func TestNoLocaleSet(t *testing.T) {
-	po = gotext.NewPo()
+	locale = gotext.NewPo()
 	require.Equal(t, "test-key", Tr("test-key"))
 }
 
diff --git a/internal/i18n/README.md b/internal/locales/README.md
similarity index 100%
rename from internal/i18n/README.md
rename to internal/locales/README.md
diff --git a/internal/i18n/cmd/ast/parser.go b/internal/locales/cmd/ast/parser.go
similarity index 97%
rename from internal/i18n/cmd/ast/parser.go
rename to internal/locales/cmd/ast/parser.go
index d8d558cd975..69648c70c1b 100644
--- a/internal/i18n/cmd/ast/parser.go
+++ b/internal/locales/cmd/ast/parser.go
@@ -24,7 +24,7 @@ import (
 	"path/filepath"
 	"strconv"
 
-	"github.com/arduino/arduino-cli/internal/i18n/cmd/po"
+	"github.com/arduino/arduino-cli/internal/locales/cmd/po"
 )
 
 // GenerateCatalog generates the i18n message catalog for the go source files
diff --git a/internal/i18n/cmd/commands/catalog/catalog.go b/internal/locales/cmd/commands/catalog/catalog.go
similarity index 100%
rename from internal/i18n/cmd/commands/catalog/catalog.go
rename to internal/locales/cmd/commands/catalog/catalog.go
diff --git a/internal/i18n/cmd/commands/catalog/generate_catalog.go b/internal/locales/cmd/commands/catalog/generate_catalog.go
similarity index 95%
rename from internal/i18n/cmd/commands/catalog/generate_catalog.go
rename to internal/locales/cmd/commands/catalog/generate_catalog.go
index 1c65c640ffc..27ac0e76d45 100644
--- a/internal/i18n/cmd/commands/catalog/generate_catalog.go
+++ b/internal/locales/cmd/commands/catalog/generate_catalog.go
@@ -19,7 +19,7 @@ import (
 	"os"
 	"path/filepath"
 
-	"github.com/arduino/arduino-cli/internal/i18n/cmd/ast"
+	"github.com/arduino/arduino-cli/internal/locales/cmd/ast"
 	"github.com/spf13/cobra"
 )
 
diff --git a/internal/i18n/cmd/commands/root.go b/internal/locales/cmd/commands/root.go
similarity index 87%
rename from internal/i18n/cmd/commands/root.go
rename to internal/locales/cmd/commands/root.go
index 14559dcfee9..fc5d8eb37e0 100644
--- a/internal/i18n/cmd/commands/root.go
+++ b/internal/locales/cmd/commands/root.go
@@ -16,8 +16,8 @@
 package commands
 
 import (
-	"github.com/arduino/arduino-cli/internal/i18n/cmd/commands/catalog"
-	"github.com/arduino/arduino-cli/internal/i18n/cmd/commands/transifex"
+	"github.com/arduino/arduino-cli/internal/locales/cmd/commands/catalog"
+	"github.com/arduino/arduino-cli/internal/locales/cmd/commands/transifex"
 	"github.com/spf13/cobra"
 )
 
diff --git a/internal/i18n/cmd/commands/transifex/pull_transifex.go b/internal/locales/cmd/commands/transifex/pull_transifex.go
similarity index 100%
rename from internal/i18n/cmd/commands/transifex/pull_transifex.go
rename to internal/locales/cmd/commands/transifex/pull_transifex.go
diff --git a/internal/i18n/cmd/commands/transifex/push_transifex.go b/internal/locales/cmd/commands/transifex/push_transifex.go
similarity index 100%
rename from internal/i18n/cmd/commands/transifex/push_transifex.go
rename to internal/locales/cmd/commands/transifex/push_transifex.go
diff --git a/internal/i18n/cmd/commands/transifex/transifex.go b/internal/locales/cmd/commands/transifex/transifex.go
similarity index 100%
rename from internal/i18n/cmd/commands/transifex/transifex.go
rename to internal/locales/cmd/commands/transifex/transifex.go
diff --git a/internal/i18n/cmd/main.go b/internal/locales/cmd/main.go
similarity index 93%
rename from internal/i18n/cmd/main.go
rename to internal/locales/cmd/main.go
index 8752c9d668e..d31f1589e09 100644
--- a/internal/i18n/cmd/main.go
+++ b/internal/locales/cmd/main.go
@@ -19,7 +19,7 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/arduino/arduino-cli/internal/i18n/cmd/commands"
+	"github.com/arduino/arduino-cli/internal/locales/cmd/commands"
 )
 
 func main() {
diff --git a/internal/i18n/cmd/po/catalog.go b/internal/locales/cmd/po/catalog.go
similarity index 100%
rename from internal/i18n/cmd/po/catalog.go
rename to internal/locales/cmd/po/catalog.go
diff --git a/internal/i18n/cmd/po/catalog_test.go b/internal/locales/cmd/po/catalog_test.go
similarity index 100%
rename from internal/i18n/cmd/po/catalog_test.go
rename to internal/locales/cmd/po/catalog_test.go
diff --git a/internal/i18n/cmd/po/merge.go b/internal/locales/cmd/po/merge.go
similarity index 100%
rename from internal/i18n/cmd/po/merge.go
rename to internal/locales/cmd/po/merge.go
diff --git a/internal/i18n/cmd/po/merge_test.go b/internal/locales/cmd/po/merge_test.go
similarity index 100%
rename from internal/i18n/cmd/po/merge_test.go
rename to internal/locales/cmd/po/merge_test.go
diff --git a/internal/i18n/cmd/po/parser.go b/internal/locales/cmd/po/parser.go
similarity index 100%
rename from internal/i18n/cmd/po/parser.go
rename to internal/locales/cmd/po/parser.go
diff --git a/internal/i18n/cmd/po/parser_test.go b/internal/locales/cmd/po/parser_test.go
similarity index 100%
rename from internal/i18n/cmd/po/parser_test.go
rename to internal/locales/cmd/po/parser_test.go
diff --git a/internal/i18n/convert.go b/internal/locales/convert.go
similarity index 98%
rename from internal/i18n/convert.go
rename to internal/locales/convert.go
index 7b6d2e8dbdf..e7aa8227571 100644
--- a/internal/i18n/convert.go
+++ b/internal/locales/convert.go
@@ -13,7 +13,7 @@
 // Arduino software without disclosing the source code of your own applications.
 // To purchase a commercial license, send an email to license@arduino.cc.
 
-package i18n
+package locales
 
 import (
 	"regexp"
diff --git a/internal/i18n/convert_test.go b/internal/locales/convert_test.go
similarity index 99%
rename from internal/i18n/convert_test.go
rename to internal/locales/convert_test.go
index 618fa977b3b..723d6118c2b 100644
--- a/internal/i18n/convert_test.go
+++ b/internal/locales/convert_test.go
@@ -13,7 +13,7 @@
 // Arduino software without disclosing the source code of your own applications.
 // To purchase a commercial license, send an email to license@arduino.cc.
 
-package i18n
+package locales
 
 import (
 	"fmt"
diff --git a/internal/i18n/data/.gitkeep b/internal/locales/data/.gitkeep
similarity index 100%
rename from internal/i18n/data/.gitkeep
rename to internal/locales/data/.gitkeep
diff --git a/internal/i18n/data/README.md b/internal/locales/data/README.md
similarity index 100%
rename from internal/i18n/data/README.md
rename to internal/locales/data/README.md
diff --git a/internal/i18n/data/ar.po b/internal/locales/data/ar.po
similarity index 100%
rename from internal/i18n/data/ar.po
rename to internal/locales/data/ar.po
diff --git a/internal/i18n/data/be.po b/internal/locales/data/be.po
similarity index 100%
rename from internal/i18n/data/be.po
rename to internal/locales/data/be.po
diff --git a/internal/i18n/data/de.po b/internal/locales/data/de.po
similarity index 100%
rename from internal/i18n/data/de.po
rename to internal/locales/data/de.po
diff --git a/internal/i18n/data/en.po b/internal/locales/data/en.po
similarity index 100%
rename from internal/i18n/data/en.po
rename to internal/locales/data/en.po
diff --git a/internal/i18n/data/es.po b/internal/locales/data/es.po
similarity index 100%
rename from internal/i18n/data/es.po
rename to internal/locales/data/es.po
diff --git a/internal/i18n/data/fr.po b/internal/locales/data/fr.po
similarity index 100%
rename from internal/i18n/data/fr.po
rename to internal/locales/data/fr.po
diff --git a/internal/i18n/data/he.po b/internal/locales/data/he.po
similarity index 100%
rename from internal/i18n/data/he.po
rename to internal/locales/data/he.po
diff --git a/internal/i18n/data/it_IT.po b/internal/locales/data/it_IT.po
similarity index 100%
rename from internal/i18n/data/it_IT.po
rename to internal/locales/data/it_IT.po
diff --git a/internal/i18n/data/ja.po b/internal/locales/data/ja.po
similarity index 100%
rename from internal/i18n/data/ja.po
rename to internal/locales/data/ja.po
diff --git a/internal/i18n/data/kk.po b/internal/locales/data/kk.po
similarity index 100%
rename from internal/i18n/data/kk.po
rename to internal/locales/data/kk.po
diff --git a/internal/i18n/data/ko.po b/internal/locales/data/ko.po
similarity index 100%
rename from internal/i18n/data/ko.po
rename to internal/locales/data/ko.po
diff --git a/internal/i18n/data/lb.po b/internal/locales/data/lb.po
similarity index 100%
rename from internal/i18n/data/lb.po
rename to internal/locales/data/lb.po
diff --git a/internal/i18n/data/mn.po b/internal/locales/data/mn.po
similarity index 100%
rename from internal/i18n/data/mn.po
rename to internal/locales/data/mn.po
diff --git a/internal/i18n/data/my_MM.po b/internal/locales/data/my_MM.po
similarity index 100%
rename from internal/i18n/data/my_MM.po
rename to internal/locales/data/my_MM.po
diff --git a/internal/i18n/data/ne.po b/internal/locales/data/ne.po
similarity index 100%
rename from internal/i18n/data/ne.po
rename to internal/locales/data/ne.po
diff --git a/internal/i18n/data/pl.po b/internal/locales/data/pl.po
similarity index 100%
rename from internal/i18n/data/pl.po
rename to internal/locales/data/pl.po
diff --git a/internal/i18n/data/pt.po b/internal/locales/data/pt.po
similarity index 100%
rename from internal/i18n/data/pt.po
rename to internal/locales/data/pt.po
diff --git a/internal/i18n/data/pt_BR.po b/internal/locales/data/pt_BR.po
similarity index 100%
rename from internal/i18n/data/pt_BR.po
rename to internal/locales/data/pt_BR.po
diff --git a/internal/i18n/data/ru.po b/internal/locales/data/ru.po
similarity index 100%
rename from internal/i18n/data/ru.po
rename to internal/locales/data/ru.po
diff --git a/internal/i18n/data/zh.po b/internal/locales/data/zh.po
similarity index 100%
rename from internal/i18n/data/zh.po
rename to internal/locales/data/zh.po
diff --git a/internal/i18n/data/zh_TW.po b/internal/locales/data/zh_TW.po
similarity index 100%
rename from internal/i18n/data/zh_TW.po
rename to internal/locales/data/zh_TW.po
diff --git a/internal/i18n/detect.go b/internal/locales/detect.go
similarity index 98%
rename from internal/i18n/detect.go
rename to internal/locales/detect.go
index f35d4f1b94e..36279f1388b 100644
--- a/internal/i18n/detect.go
+++ b/internal/locales/detect.go
@@ -13,7 +13,7 @@
 // Arduino software without disclosing the source code of your own applications.
 // To purchase a commercial license, send an email to license@arduino.cc.
 
-package i18n
+package locales
 
 import (
 	"os"
diff --git a/internal/i18n/detect_cgo_darwin.go b/internal/locales/detect_cgo_darwin.go
similarity index 98%
rename from internal/i18n/detect_cgo_darwin.go
rename to internal/locales/detect_cgo_darwin.go
index c9fd4ecefb9..7c613cf6135 100644
--- a/internal/i18n/detect_cgo_darwin.go
+++ b/internal/locales/detect_cgo_darwin.go
@@ -15,7 +15,7 @@
 
 //go:build darwin && cgo
 
-package i18n
+package locales
 
 /*
 #cgo CFLAGS: -x objective-c
diff --git a/internal/i18n/detect_freebsd.go b/internal/locales/detect_freebsd.go
similarity index 98%
rename from internal/i18n/detect_freebsd.go
rename to internal/locales/detect_freebsd.go
index 759509c5abe..4d03b8f3c92 100644
--- a/internal/i18n/detect_freebsd.go
+++ b/internal/locales/detect_freebsd.go
@@ -13,7 +13,7 @@
 // Arduino software without disclosing the source code of your own applications.
 // To purchase a commercial license, send an email to license@arduino.cc.
 
-package i18n
+package locales
 
 func getLocaleIdentifier() string {
 	return getLocaleIdentifierFromEnv()
diff --git a/internal/i18n/detect_linux.go b/internal/locales/detect_linux.go
similarity index 98%
rename from internal/i18n/detect_linux.go
rename to internal/locales/detect_linux.go
index 759509c5abe..4d03b8f3c92 100644
--- a/internal/i18n/detect_linux.go
+++ b/internal/locales/detect_linux.go
@@ -13,7 +13,7 @@
 // Arduino software without disclosing the source code of your own applications.
 // To purchase a commercial license, send an email to license@arduino.cc.
 
-package i18n
+package locales
 
 func getLocaleIdentifier() string {
 	return getLocaleIdentifierFromEnv()
diff --git a/internal/i18n/detect_nocgo_darwin.go b/internal/locales/detect_nocgo_darwin.go
similarity index 98%
rename from internal/i18n/detect_nocgo_darwin.go
rename to internal/locales/detect_nocgo_darwin.go
index f7ae977b19f..689b5e864c5 100644
--- a/internal/i18n/detect_nocgo_darwin.go
+++ b/internal/locales/detect_nocgo_darwin.go
@@ -15,7 +15,7 @@
 
 //go:build darwin && !cgo
 
-package i18n
+package locales
 
 func getLocaleIdentifier() string {
 	return getLocaleIdentifierFromEnv()
diff --git a/internal/i18n/detect_windows.go b/internal/locales/detect_windows.go
similarity index 91%
rename from internal/i18n/detect_windows.go
rename to internal/locales/detect_windows.go
index 84bc3af7997..42c667447d6 100644
--- a/internal/i18n/detect_windows.go
+++ b/internal/locales/detect_windows.go
@@ -13,20 +13,18 @@
 // Arduino software without disclosing the source code of your own applications.
 // To purchase a commercial license, send an email to license@arduino.cc.
 
-package i18n
+package locales
 
 import (
 	"strings"
 	"syscall"
 	"unsafe"
-
-	"github.com/sirupsen/logrus"
 )
 
 func getLocaleIdentifier() string {
 	defer func() {
 		if r := recover(); r != nil {
-			logrus.WithField("error", r).Errorf("Failed to get windows user locale")
+			// ignore error and do not panic
 		}
 	}()
 
diff --git a/internal/locales/i18n.go b/internal/locales/i18n.go
new file mode 100644
index 00000000000..8c7120a1771
--- /dev/null
+++ b/internal/locales/i18n.go
@@ -0,0 +1,39 @@
+// This file is part of arduino-cli.
+//
+// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
+//
+// This software is released under the GNU General Public License version 3,
+// which covers the main part of arduino-cli.
+// The terms of this license can be found at:
+// https://www.gnu.org/licenses/gpl-3.0.en.html
+//
+// You can be released from the requirements of the above licenses by purchasing
+// a commercial license. Buying such a license is mandatory if you want to
+// modify or otherwise use the software for commercial activities involving the
+// Arduino software without disclosing the source code of your own applications.
+// To purchase a commercial license, send an email to license@arduino.cc.
+
+package locales
+
+// Init initializes the i18n module, setting the locale according to this order of preference:
+// 1. Locale specified via the function call
+// 2. OS Locale
+// 3. en (default)
+func Init(configLocale string) {
+	locales := supportedLocales()
+	if configLocale != "" {
+		if locale := findMatchingLocale(configLocale, locales); locale != "" {
+			setLocale(locale)
+			return
+		}
+	}
+
+	if osLocale := getLocaleIdentifierFromOS(); osLocale != "" {
+		if locale := findMatchingLocale(osLocale, locales); locale != "" {
+			setLocale(locale)
+			return
+		}
+	}
+
+	setLocale("en")
+}
diff --git a/internal/i18n/locale.go b/internal/locales/locale.go
similarity index 93%
rename from internal/i18n/locale.go
rename to internal/locales/locale.go
index 35a43e42e03..646b44059f7 100644
--- a/internal/i18n/locale.go
+++ b/internal/locales/locale.go
@@ -13,24 +13,19 @@
 // Arduino software without disclosing the source code of your own applications.
 // To purchase a commercial license, send an email to license@arduino.cc.
 
-package i18n
+package locales
 
 import (
 	"embed"
 	"strings"
 
+	"github.com/arduino/arduino-cli/internal/i18n"
 	"github.com/leonelquinteros/gotext"
 )
 
-var po *gotext.Po
-
 //go:embed data/*.po
 var contents embed.FS
 
-func init() {
-	po = gotext.NewPo()
-}
-
 func supportedLocales() []string {
 	var locales []string
 	files, err := contents.ReadDir("data")
@@ -75,6 +70,7 @@ func setLocale(locale string) {
 	if err != nil {
 		panic("Error reading embedded i18n data: " + err.Error())
 	}
-	po = gotext.NewPo()
-	po.Parse(poFile)
+	dict := gotext.NewPo()
+	dict.Parse(poFile)
+	i18n.SetLocale(dict)
 }
diff --git a/internal/i18n/locale_test.go b/internal/locales/locale_test.go
similarity index 98%
rename from internal/i18n/locale_test.go
rename to internal/locales/locale_test.go
index 6212258e8f8..dc0ff7ad897 100644
--- a/internal/i18n/locale_test.go
+++ b/internal/locales/locale_test.go
@@ -13,7 +13,7 @@
 // Arduino software without disclosing the source code of your own applications.
 // To purchase a commercial license, send an email to license@arduino.cc.
 
-package i18n
+package locales
 
 import (
 	"testing"
diff --git a/main.go b/main.go
index 84896ac19fe..0ead329254a 100644
--- a/main.go
+++ b/main.go
@@ -27,6 +27,7 @@ import (
 	"github.com/arduino/arduino-cli/internal/cli/configuration"
 	"github.com/arduino/arduino-cli/internal/cli/feedback"
 	"github.com/arduino/arduino-cli/internal/i18n"
+	"github.com/arduino/arduino-cli/internal/locales"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	"github.com/arduino/go-paths-helper"
 	"github.com/sirupsen/logrus"
@@ -67,7 +68,7 @@ func main() {
 	config := resp.GetConfiguration()
 
 	// Setup i18n
-	i18n.Init(config.GetLocale())
+	locales.Init(config.GetLocale())
 
 	// Setup command line parser with the server and settings
 	arduinoCmd := cli.NewCommand(srv)
diff --git a/internal/arduino/cores/fqbn.go b/pkg/fqbn/fqbn.go
similarity index 77%
rename from internal/arduino/cores/fqbn.go
rename to pkg/fqbn/fqbn.go
index 0db32f45cb0..8773272dd04 100644
--- a/internal/arduino/cores/fqbn.go
+++ b/pkg/fqbn/fqbn.go
@@ -13,7 +13,7 @@
 // Arduino software without disclosing the source code of your own applications.
 // To purchase a commercial license, send an email to license@arduino.cc.
 
-package cores
+package fqbn
 
 import (
 	"errors"
@@ -24,35 +24,38 @@ import (
 	properties "github.com/arduino/go-properties-orderedmap"
 )
 
-// FQBN represents a Board with a specific configuration
+// FQBN represents an Fully Qualified Board Name string
 type FQBN struct {
-	Package      string
-	PlatformArch string
+	Packager     string
+	Architecture string
 	BoardID      string
 	Configs      *properties.Map
 }
 
-// MustParseFQBN extract an FQBN object from the input string
+// MustParse parse an FQBN string from the input string
 // or panics if the input is not a valid FQBN.
-func MustParseFQBN(fqbnIn string) *FQBN {
-	res, err := ParseFQBN(fqbnIn)
+func MustParse(fqbnIn string) *FQBN {
+	res, err := Parse(fqbnIn)
 	if err != nil {
 		panic(err)
 	}
 	return res
 }
 
-// ParseFQBN extract an FQBN object from the input string
-func ParseFQBN(fqbnIn string) (*FQBN, error) {
-	// Split fqbn
+var fqbnValidationRegex = regexp.MustCompile(`^[a-zA-Z0-9_.-]*$`)
+var valueValidationRegex = regexp.MustCompile(`^[a-zA-Z0-9=_.-]*$`)
+
+// Parse parses an FQBN string from the input string
+func Parse(fqbnIn string) (*FQBN, error) {
+	// Split fqbn parts
 	fqbnParts := strings.Split(fqbnIn, ":")
 	if len(fqbnParts) < 3 || len(fqbnParts) > 4 {
 		return nil, errors.New(i18n.Tr("not an FQBN: %s", fqbnIn))
 	}
 
 	fqbn := &FQBN{
-		Package:      fqbnParts[0],
-		PlatformArch: fqbnParts[1],
+		Packager:     fqbnParts[0],
+		Architecture: fqbnParts[1],
 		BoardID:      fqbnParts[2],
 		Configs:      properties.NewMap(),
 	}
@@ -60,7 +63,6 @@ func ParseFQBN(fqbnIn string) (*FQBN, error) {
 		return nil, errors.New(i18n.Tr("empty board identifier"))
 	}
 	// Check if the fqbn contains invalid characters
-	fqbnValidationRegex := regexp.MustCompile(`^[a-zA-Z0-9_.-]*$`)
 	for i := 0; i < 3; i++ {
 		if !fqbnValidationRegex.MatchString(fqbnParts[i]) {
 			return nil, errors.New(i18n.Tr("fqbn's field %s contains an invalid character", fqbnParts[i]))
@@ -81,7 +83,6 @@ func ParseFQBN(fqbnIn string) (*FQBN, error) {
 				return nil, errors.New(i18n.Tr("config key %s contains an invalid character", k))
 			}
 			// The config value can also contain the = symbol
-			valueValidationRegex := regexp.MustCompile(`^[a-zA-Z0-9=_.-]*$`)
 			if !valueValidationRegex.MatchString(v) {
 				return nil, errors.New(i18n.Tr("config value %s contains an invalid character", v))
 			}
@@ -91,29 +92,17 @@ func ParseFQBN(fqbnIn string) (*FQBN, error) {
 	return fqbn, nil
 }
 
-func (fqbn *FQBN) String() string {
-	res := fqbn.StringWithoutConfig()
-	if fqbn.Configs.Size() > 0 {
-		sep := ":"
-		for _, k := range fqbn.Configs.Keys() {
-			res += sep + k + "=" + fqbn.Configs.Get(k)
-			sep = ","
-		}
-	}
-	return res
-}
-
 // Clone returns a copy of this FQBN.
 func (fqbn *FQBN) Clone() *FQBN {
 	return &FQBN{
-		Package:      fqbn.Package,
-		PlatformArch: fqbn.PlatformArch,
+		Packager:     fqbn.Packager,
+		Architecture: fqbn.Architecture,
 		BoardID:      fqbn.BoardID,
 		Configs:      fqbn.Configs.Clone(),
 	}
 }
 
-// Match check if the target FQBN corresponds to the receiver one.
+// Match checks if the target FQBN equals to this one.
 // The core parts are checked for exact equality while board options are loosely
 // matched: the set of boards options of the target must be fully contained within
 // the one of the receiver and their values must be equal.
@@ -122,10 +111,8 @@ func (fqbn *FQBN) Match(target *FQBN) bool {
 		return false
 	}
 
-	searchedProperties := target.Configs.Clone()
-	actualConfigs := fqbn.Configs.AsMap()
-	for neededKey, neededValue := range searchedProperties.AsMap() {
-		targetValue, hasKey := actualConfigs[neededKey]
+	for neededKey, neededValue := range target.Configs.AsMap() {
+		targetValue, hasKey := fqbn.Configs.GetOk(neededKey)
 		if !hasKey || targetValue != neededValue {
 			return false
 		}
@@ -135,5 +122,18 @@ func (fqbn *FQBN) Match(target *FQBN) bool {
 
 // StringWithoutConfig returns the FQBN without the Config part
 func (fqbn *FQBN) StringWithoutConfig() string {
-	return fqbn.Package + ":" + fqbn.PlatformArch + ":" + fqbn.BoardID
+	return fqbn.Packager + ":" + fqbn.Architecture + ":" + fqbn.BoardID
+}
+
+// String returns the FQBN as a string
+func (fqbn *FQBN) String() string {
+	res := fqbn.StringWithoutConfig()
+	if fqbn.Configs.Size() > 0 {
+		sep := ":"
+		for _, k := range fqbn.Configs.Keys() {
+			res += sep + k + "=" + fqbn.Configs.Get(k)
+			sep = ","
+		}
+	}
+	return res
 }
diff --git a/internal/arduino/cores/fqbn_test.go b/pkg/fqbn/fqbn_test.go
similarity index 60%
rename from internal/arduino/cores/fqbn_test.go
rename to pkg/fqbn/fqbn_test.go
index c7165af064a..be76dad2f27 100644
--- a/internal/arduino/cores/fqbn_test.go
+++ b/pkg/fqbn/fqbn_test.go
@@ -13,67 +13,68 @@
 // Arduino software without disclosing the source code of your own applications.
 // To purchase a commercial license, send an email to license@arduino.cc.
 
-package cores
+package fqbn_test
 
 import (
 	"testing"
 
+	"github.com/arduino/arduino-cli/pkg/fqbn"
 	"github.com/stretchr/testify/require"
 )
 
 func TestFQBN(t *testing.T) {
-	a, err := ParseFQBN("arduino:avr:uno")
+	a, err := fqbn.Parse("arduino:avr:uno")
 	require.Equal(t, "arduino:avr:uno", a.String())
 	require.NoError(t, err)
-	require.Equal(t, a.Package, "arduino")
-	require.Equal(t, a.PlatformArch, "avr")
+	require.Equal(t, a.Packager, "arduino")
+	require.Equal(t, a.Architecture, "avr")
 	require.Equal(t, a.BoardID, "uno")
 	require.Zero(t, a.Configs.Size())
 
 	// Allow empty platforms or packages (aka. vendors + architectures)
-	b1, err := ParseFQBN("arduino::uno")
+	b1, err := fqbn.Parse("arduino::uno")
 	require.Equal(t, "arduino::uno", b1.String())
 	require.NoError(t, err)
-	require.Equal(t, b1.Package, "arduino")
-	require.Equal(t, b1.PlatformArch, "")
+	require.Equal(t, b1.Packager, "arduino")
+	require.Equal(t, b1.Architecture, "")
 	require.Equal(t, b1.BoardID, "uno")
 	require.Zero(t, b1.Configs.Size())
 
-	b2, err := ParseFQBN(":avr:uno")
+	b2, err := fqbn.Parse(":avr:uno")
 	require.Equal(t, ":avr:uno", b2.String())
 	require.NoError(t, err)
-	require.Equal(t, b2.Package, "")
-	require.Equal(t, b2.PlatformArch, "avr")
+	require.Equal(t, b2.Packager, "")
+	require.Equal(t, b2.Architecture, "avr")
 	require.Equal(t, b2.BoardID, "uno")
 	require.Zero(t, b2.Configs.Size())
 
-	b3, err := ParseFQBN("::uno")
+	b3, err := fqbn.Parse("::uno")
 	require.Equal(t, "::uno", b3.String())
 	require.NoError(t, err)
-	require.Equal(t, b3.Package, "")
-	require.Equal(t, b3.PlatformArch, "")
+	require.Equal(t, b3.Packager, "")
+	require.Equal(t, b3.Architecture, "")
 	require.Equal(t, b3.BoardID, "uno")
 	require.Zero(t, b3.Configs.Size())
 
 	// Do not allow missing board identifier
-	_, err = ParseFQBN("arduino:avr:")
+	_, err = fqbn.Parse("arduino:avr:")
 	require.Error(t, err)
 
 	// Do not allow partial fqbn
-	_, err = ParseFQBN("arduino")
+	_, err = fqbn.Parse("arduino")
 	require.Error(t, err)
-	_, err = ParseFQBN("arduino:avr")
+	_, err = fqbn.Parse("arduino:avr")
 	require.Error(t, err)
 
 	// Keeps the config keys order
-	s1, err := ParseFQBN("arduino:avr:uno:d=x,b=x,a=x,e=x,c=x")
+	s1, err := fqbn.Parse("arduino:avr:uno:d=x,b=x,a=x,e=x,c=x")
 	require.NoError(t, err)
 	require.Equal(t, "arduino:avr:uno:d=x,b=x,a=x,e=x,c=x", s1.String())
 	require.Equal(t,
 		"properties.Map{\n  \"d\": \"x\",\n  \"b\": \"x\",\n  \"a\": \"x\",\n  \"e\": \"x\",\n  \"c\": \"x\",\n}",
 		s1.Configs.Dump())
 
-	s2, err := ParseFQBN("arduino:avr:uno:a=x,b=x,c=x,d=x,e=x")
+	s2, err := fqbn.Parse("arduino:avr:uno:a=x,b=x,c=x,d=x,e=x")
 	require.NoError(t, err)
 	require.Equal(t, "arduino:avr:uno:a=x,b=x,c=x,d=x,e=x", s2.String())
 	require.Equal(t,
@@ -85,57 +86,78 @@ func TestFQBN(t *testing.T) {
 	require.NotEqual(t, s1.String(), s2.String())
 
 	// Test configs
-	c, err := ParseFQBN("arduino:avr:uno:cpu=atmega")
+	c, err := fqbn.Parse("arduino:avr:uno:cpu=atmega")
 	require.Equal(t, "arduino:avr:uno:cpu=atmega", c.String())
 	require.NoError(t, err)
-	require.Equal(t, c.Package, "arduino")
-	require.Equal(t, c.PlatformArch, "avr")
+	require.Equal(t, c.Packager, "arduino")
+	require.Equal(t, c.Architecture, "avr")
 	require.Equal(t, c.BoardID, "uno")
 	require.Equal(t, "properties.Map{\n  \"cpu\": \"atmega\",\n}", c.Configs.Dump())
 
-	d, err := ParseFQBN("arduino:avr:uno:cpu=atmega,speed=1000")
+	d, err := fqbn.Parse("arduino:avr:uno:cpu=atmega,speed=1000")
 	require.Equal(t, "arduino:avr:uno:cpu=atmega,speed=1000", d.String())
 	require.NoError(t, err)
-	require.Equal(t, d.Package, "arduino")
-	require.Equal(t, d.PlatformArch, "avr")
+	require.Equal(t, d.Packager, "arduino")
+	require.Equal(t, d.Architecture, "avr")
 	require.Equal(t, d.BoardID, "uno")
 	require.Equal(t, "properties.Map{\n  \"cpu\": \"atmega\",\n  \"speed\": \"1000\",\n}", d.Configs.Dump())
 
 	// Do not allow empty keys or missing values in config
-	_, err = ParseFQBN("arduino:avr:uno:")
+	_, err = fqbn.Parse("arduino:avr:uno:")
 	require.Error(t, err)
-	_, err = ParseFQBN("arduino:avr:uno,")
+	_, err = fqbn.Parse("arduino:avr:uno,")
 	require.Error(t, err)
-	_, err = ParseFQBN("arduino:avr:uno:cpu")
+	_, err = fqbn.Parse("arduino:avr:uno:cpu")
 	require.Error(t, err)
-	_, err = ParseFQBN("arduino:avr:uno:=atmega")
+	_, err = fqbn.Parse("arduino:avr:uno:=atmega")
 	require.Error(t, err)
-	_, err = ParseFQBN("arduino:avr:uno:cpu=atmega,")
+	_, err = fqbn.Parse("arduino:avr:uno:cpu=atmega,")
 	require.Error(t, err)
-	_, err = ParseFQBN("arduino:avr:uno:cpu=atmega,speed")
+	_, err = fqbn.Parse("arduino:avr:uno:cpu=atmega,speed")
 	require.Error(t, err)
-	_, err = ParseFQBN("arduino:avr:uno:cpu=atmega,=1000")
+	_, err = fqbn.Parse("arduino:avr:uno:cpu=atmega,=1000")
 	require.Error(t, err)
 
 	// Allow keys with empty values
-	e, err := ParseFQBN("arduino:avr:uno:cpu=")
+	e, err := fqbn.Parse("arduino:avr:uno:cpu=")
 	require.Equal(t, "arduino:avr:uno:cpu=", e.String())
 	require.NoError(t, err)
-	require.Equal(t, e.Package, "arduino")
-	require.Equal(t, e.PlatformArch, "avr")
+	require.Equal(t, e.Packager, "arduino")
+	require.Equal(t, e.Architecture, "avr")
 	require.Equal(t, e.BoardID, "uno")
 	require.Equal(t, "properties.Map{\n  \"cpu\": \"\",\n}", e.Configs.Dump())
 
 	// Allow "=" in config values
-	f, err := ParseFQBN("arduino:avr:uno:cpu=atmega,speed=1000,extra=core=arduino")
+	f, err := fqbn.Parse("arduino:avr:uno:cpu=atmega,speed=1000,extra=core=arduino")
 	require.Equal(t, "arduino:avr:uno:cpu=atmega,speed=1000,extra=core=arduino", f.String())
 	require.NoError(t, err)
-	require.Equal(t, f.Package, "arduino")
-	require.Equal(t, f.PlatformArch, "avr")
+	require.Equal(t, f.Packager, "arduino")
+	require.Equal(t, f.Architecture, "avr")
 	require.Equal(t, f.BoardID, "uno")
 	require.Equal(t,
 		"properties.Map{\n  \"cpu\": \"atmega\",\n  \"speed\": \"1000\",\n  \"extra\": \"core=arduino\",\n}",
 		f.Configs.Dump())
+
+	// Check invalid characters in config keys
+	_, err = fqbn.Parse("arduino:avr:uno:cpu@=atmega")
+	require.Error(t, err)
+	_, err = fqbn.Parse("arduino:avr:uno:cpu@atmega")
+	require.Error(t, err)
+	_, err = fqbn.Parse("arduino:avr:uno:cpu=atmega,speed@=1000")
+	require.Error(t, err)
+	_, err = fqbn.Parse("arduino:avr:uno:cpu=atmega,speed@1000")
+	require.Error(t, err)
+
+	// Check invalid characters in config values
+	_, err = fqbn.Parse("arduino:avr:uno:cpu=atmega@")
+	require.Error(t, err)
+	_, err = fqbn.Parse("arduino:avr:uno:cpu=atmega@extra")
+	require.Error(t, err)
+	_, err = fqbn.Parse("arduino:avr:uno:cpu=atmega,speed=1000@")
+	require.Error(t, err)
+	_, err = fqbn.Parse("arduino:avr:uno:cpu=atmega,speed=1000@extra")
+	require.Error(t, err)
+
 }
 
 func TestMatch(t *testing.T) {
@@ -148,9 +170,9 @@ func TestMatch(t *testing.T) {
 	}
 
 	for _, pair := range expectedMatches {
-		a, err := ParseFQBN(pair[0])
+		a, err := fqbn.Parse(pair[0])
 		require.NoError(t, err)
-		b, err := ParseFQBN(pair[1])
+		b, err := fqbn.Parse(pair[1])
 		require.NoError(t, err)
 		require.True(t, b.Match(a))
 	}
@@ -164,9 +186,9 @@ func TestMatch(t *testing.T) {
 	}
 
 	for _, pair := range expectedMismatches {
-		a, err := ParseFQBN(pair[0])
+		a, err := fqbn.Parse(pair[0])
 		require.NoError(t, err)
-		b, err := ParseFQBN(pair[1])
+		b, err := fqbn.Parse(pair[1])
 		require.NoError(t, err)
 		require.False(t, b.Match(a))
 	}
@@ -175,14 +197,32 @@ func TestMatch(t *testing.T) {
 func TestValidCharacters(t *testing.T) {
 	// These FQBNs contain valid characters
 	validFqbns := []string{"ardui_no:av_r:un_o", "arduin.o:av.r:un.o", "arduin-o:av-r:un-o", "arduin-o:av-r:un-o:a=b=c=d"}
-	for _, fqbn := range validFqbns {
-		_, err := ParseFQBN(fqbn)
+	for _, validFqbn := range validFqbns {
+		_, err := fqbn.Parse(validFqbn)
 		require.NoError(t, err)
 	}
 	// These FQBNs contain invalid characters
 	invalidFqbns := []string{"arduin-o:av-r:un=o", "arduin?o:av-r:uno", "arduino:av*r:uno"}
-	for _, fqbn := range invalidFqbns {
-		_, err := ParseFQBN(fqbn)
+	for _, validFqbn := range invalidFqbns {
+		_, err := fqbn.Parse(validFqbn)
 		require.Error(t, err)
 	}
 }
+
+func TestMustParse(t *testing.T) {
+	require.NotPanics(t, func() { fqbn.MustParse("arduino:avr:uno") })
+	require.Panics(t, func() { fqbn.MustParse("ard=uino:avr=:u=no") })
+}
+
+func TestClone(t *testing.T) {
+	a, err := fqbn.Parse("arduino:avr:uno:opt1=1,opt2=2")
+	require.NoError(t, err)
+	b := a.Clone()
+	require.True(t, b.Match(a))
+	require.True(t, a.Match(b))
+
+	c, err := fqbn.Parse("arduino:avr:uno:opt1=1,opt2=2,opt3=3")
+	require.NoError(t, err)
+	require.True(t, c.Match(a))
+	require.False(t, a.Match(c))
+}