├── .husky
└── commit-msg
├── app.plugin.js
├── android
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── expo
│ │ └── modules
│ │ └── pluginlocalization
│ │ └── ExpoPluginLocalizationModule.kt
└── build.gradle
├── commitlint.config.js
├── .vscode
└── settings.json
├── example
├── assets
│ ├── icon.png
│ ├── favicon.png
│ ├── splash.png
│ └── adaptive-icon.png
├── tsconfig.json
├── .gitignore
├── App.tsx
├── babel.config.js
├── package.json
├── app.json
└── README.md
├── .eslintrc.js
├── .npmignore
├── plugin
├── tsconfig.json
└── src
│ ├── withAndroidLocalizableManifest.ts
│ ├── withIosLocalizableProject.ts
│ ├── withAndroidLocalizableResources.ts
│ ├── withIosLocalizableResources.ts
│ ├── index.ts
│ └── withAndroidLocalizableGradle.ts
├── expo-module.config.json
├── tsconfig.json
├── src
├── index.ts
└── ExpoPluginLocalizationModule.ts
├── ios
├── ExpoPluginLocalizationModule.swift
└── ExpoPluginLocalization.podspec
├── .github
└── workflows
│ └── release-please.yml
├── SECURITY.md
├── .gitignore
├── package.json
├── CHANGELOG.md
├── README.md
└── LICENSE
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | npx --no -- commitlint --edit ${1}
2 |
--------------------------------------------------------------------------------
/app.plugin.js:
--------------------------------------------------------------------------------
1 | module.exports = require("./plugin/build");
2 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ["@commitlint/config-conventional"] };
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.codeActionsOnSave": {
3 | "source.fixAll.eslint": "explicit"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/digitalartlab/expo-plugin-localization/HEAD/example/assets/icon.png
--------------------------------------------------------------------------------
/example/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/digitalartlab/expo-plugin-localization/HEAD/example/assets/favicon.png
--------------------------------------------------------------------------------
/example/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/digitalartlab/expo-plugin-localization/HEAD/example/assets/splash.png
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ["universe/native"],
4 | ignorePatterns: ["build"],
5 | };
6 |
--------------------------------------------------------------------------------
/example/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/digitalartlab/expo-plugin-localization/HEAD/example/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Exclude all top-level hidden directories by convention
2 | /.*/
3 |
4 | __mocks__
5 | __tests__
6 |
7 | /babel.config.js
8 | /commitlint.config.js
9 | /android/src/androidTest/
10 | /android/src/test/
11 | /android/build/
12 | /example/
13 |
--------------------------------------------------------------------------------
/plugin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo-module-scripts/tsconfig.plugin",
3 | "compilerOptions": {
4 | "outDir": "build",
5 | "rootDir": "src"
6 | },
7 | "include": ["./src"],
8 | "exclude": ["**/__mocks__/*", "**/__tests__/*"]
9 | }
10 |
--------------------------------------------------------------------------------
/expo-module.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "platforms": ["ios", "android"],
3 | "ios": {
4 | "modules": ["ExpoPluginLocalizationModule"]
5 | },
6 | "android": {
7 | "modules": ["expo.modules.pluginlocalization.ExpoPluginLocalizationModule"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | // @generated by expo-module-scripts
2 | {
3 | "extends": "expo-module-scripts/tsconfig.base",
4 | "compilerOptions": {
5 | "outDir": "./build"
6 | },
7 | "include": ["./src"],
8 | "exclude": ["**/__mocks__/*", "**/__tests__/*"]
9 | }
10 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "expo-plugin-localization": ["../src/index"],
7 | "expo-plugin-localization/*": ["../src/*"]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import ExpoPluginLocalizationModule from "./ExpoPluginLocalizationModule";
2 |
3 | export function getLocales(): string[] {
4 | const localesString: string = ExpoPluginLocalizationModule.getLocales();
5 | const locales = localesString.split(",");
6 |
7 | return locales;
8 | }
9 |
--------------------------------------------------------------------------------
/src/ExpoPluginLocalizationModule.ts:
--------------------------------------------------------------------------------
1 | import { requireNativeModule } from "expo-modules-core";
2 |
3 | // It loads the native module object from the JSI or falls back to
4 | // the bridge module (from NativeModulesProxy) if the remote debugger is on.
5 | export default requireNativeModule("ExpoPluginLocalization");
6 |
--------------------------------------------------------------------------------
/ios/ExpoPluginLocalizationModule.swift:
--------------------------------------------------------------------------------
1 | import ExpoModulesCore
2 |
3 | public class ExpoPluginLocalizationModule: Module {
4 | public func definition() -> ModuleDefinition {
5 | Name("ExpoPluginLocalization")
6 |
7 | Function("getLocales") {
8 | return Bundle.main.object(forInfoDictionaryKey: "LOCALES_SUPPORTED") as? String
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.github/workflows/release-please.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - main
5 |
6 | permissions:
7 | contents: write
8 | pull-requests: write
9 |
10 | name: release-please
11 |
12 | jobs:
13 | release-please:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: google-github-actions/release-please-action@v3
17 | with:
18 | release-type: node
19 | package-name: "@digitalartlab/expo-plugin-localization"
20 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 |
10 | # Native
11 | *.orig.*
12 | *.jks
13 | *.p8
14 | *.p12
15 | *.key
16 | *.mobileprovision
17 |
18 | # Metro
19 | .metro-health-check*
20 |
21 | # debug
22 | npm-debug.*
23 | yarn-debug.*
24 | yarn-error.*
25 |
26 | # macOS
27 | .DS_Store
28 | *.pem
29 |
30 | # local env files
31 | .env*.local
32 |
33 | # typescript
34 | *.tsbuildinfo
35 |
36 | # prebuild
37 | ios
38 | android
39 |
--------------------------------------------------------------------------------
/example/App.tsx:
--------------------------------------------------------------------------------
1 | import * as Linking from "expo-linking";
2 | import { getLocales } from "expo-plugin-localization";
3 | import { Button, Text, View } from "react-native";
4 |
5 | export default function App() {
6 | return (
7 |
8 |
9 | Supported locales:{" "}
10 | {getLocales()
11 | .map((locale) => locale)
12 | .join(", ")}
13 |
14 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | ------- | ------------------ |
7 | | 3.0.x | :white_check_mark: |
8 | | 2.0.x | :x: |
9 | | 1.0.x | :x: |
10 | | 0.1.x | :x: |
11 |
12 | ## Reporting a Vulnerability
13 |
14 | To report a vulnerability, please use the "Report a vulnerability" button on the [Security page on GitHub](https://github.com/digitalartlab/expo-plugin-localization/security). This will create a private issue in the repository, which only the maintainers can see. The maintainers will then respond to the issue as soon as possible.
15 |
--------------------------------------------------------------------------------
/example/babel.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | module.exports = function (api) {
3 | api.cache(true);
4 | return {
5 | presets: ["babel-preset-expo"],
6 | plugins: [
7 | [
8 | "module-resolver",
9 | {
10 | extensions: [".tsx", ".ts", ".js", ".json"],
11 | alias: {
12 | // For development, we want to alias the library to the source
13 | "expo-plugin-localization": path.join(
14 | __dirname,
15 | "..",
16 | "src",
17 | "index.ts",
18 | ),
19 | },
20 | },
21 | ],
22 | ],
23 | };
24 | };
25 |
--------------------------------------------------------------------------------
/android/src/main/java/expo/modules/pluginlocalization/ExpoPluginLocalizationModule.kt:
--------------------------------------------------------------------------------
1 | package expo.modules.pluginlocalization
2 |
3 | import expo.modules.kotlin.modules.Module
4 | import expo.modules.kotlin.modules.ModuleDefinition
5 | import android.content.pm.PackageManager
6 |
7 | class ExpoPluginLocalizationModule() : Module() {
8 | override fun definition() = ModuleDefinition {
9 | Name("ExpoPluginLocalization")
10 |
11 | Function("getLocales") {
12 | val applicationInfo = appContext?.reactContext?.packageManager?.getApplicationInfo(appContext?.reactContext?.packageName.toString(), PackageManager.GET_META_DATA)
13 |
14 | return@Function applicationInfo?.metaData?.getString("LOCALES_SUPPORTED")
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/plugin/src/withAndroidLocalizableManifest.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withAndroidManifest } from "expo/config-plugins";
2 |
3 | /**
4 | * Add reference to the locales_config.xml file in the AndroidManifest.xml
5 | */
6 | export const withAndroidLocalizableManifest: ConfigPlugin = (config) => {
7 | return withAndroidManifest(config, (config) => {
8 | const androidManifest = config.modResults;
9 | const applications = androidManifest.manifest.application;
10 | if (!applications || !applications[0]) {
11 | throw new Error(
12 | `Cannot configure localization because the AndroidManifest.xml is missing an tag`,
13 | );
14 | }
15 |
16 | applications[0].$["android:localeConfig"] = "@xml/locales_config";
17 | return config;
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "expo-plugin-localization-example",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo run:android",
8 | "ios": "expo run:ios"
9 | },
10 | "dependencies": {
11 | "babel-plugin-module-resolver": "^5.0.2",
12 | "expo": "^51.0.0",
13 | "expo-linking": "~6.3.1",
14 | "expo-splash-screen": "~0.27.5",
15 | "expo-status-bar": "~1.12.1",
16 | "react": "18.2.0",
17 | "react-native": "0.74.3"
18 | },
19 | "devDependencies": {
20 | "@babel/core": "^7.20.0",
21 | "@types/react": "~18.2.14",
22 | "typescript": "~5.3.3"
23 | },
24 | "private": true,
25 | "expo": {
26 | "autolinking": {
27 | "nativeModulesDir": ".."
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # VSCode
6 | .vscode/
7 | !.vscode/settings.json
8 | jsconfig.json
9 |
10 | # Xcode
11 | #
12 | build/
13 | *.pbxuser
14 | !default.pbxuser
15 | *.mode1v3
16 | !default.mode1v3
17 | *.mode2v3
18 | !default.mode2v3
19 | *.perspectivev3
20 | !default.perspectivev3
21 | xcuserdata
22 | *.xccheckout
23 | *.moved-aside
24 | DerivedData
25 | *.hmap
26 | *.ipa
27 | *.xcuserstate
28 | project.xcworkspace
29 |
30 | # Android/IJ
31 | #
32 | .classpath
33 | .cxx
34 | .gradle
35 | .idea
36 | .project
37 | .settings
38 | local.properties
39 | android.iml
40 | android/app/libs
41 | android/keystores/debug.keystore
42 |
43 | # Cocoapods
44 | #
45 | example/ios/Pods
46 |
47 | # Ruby
48 | example/vendor/
49 |
50 | # node.js
51 | #
52 | node_modules/
53 | npm-debug.log
54 | yarn-debug.log
55 | yarn-error.log
56 |
57 | # Expo
58 | .expo/*
59 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "expo-plugin-localization-example",
4 | "slug": "expo-plugin-localization-example",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "userInterfaceStyle": "light",
9 | "splash": {
10 | "image": "./assets/splash.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#ffffff"
13 | },
14 | "assetBundlePatterns": [
15 | "**/*"
16 | ],
17 | "ios": {
18 | "supportsTablet": true,
19 | "bundleIdentifier": "expo.modules.pluginlocalization.example"
20 | },
21 | "android": {
22 | "adaptiveIcon": {
23 | "foregroundImage": "./assets/adaptive-icon.png",
24 | "backgroundColor": "#ffffff"
25 | },
26 | "package": "expo.modules.pluginlocalization.example"
27 | },
28 | "plugins": [["../app.plugin.js", { "locales": ["nl", "es", "en"] }]]
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/ios/ExpoPluginLocalization.podspec:
--------------------------------------------------------------------------------
1 | require 'json'
2 |
3 | package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4 |
5 | Pod::Spec.new do |s|
6 | s.name = 'ExpoPluginLocalization'
7 | s.version = package['version']
8 | s.summary = package['description']
9 | s.description = package['description']
10 | s.license = package['license']
11 | s.author = package['author']
12 | s.homepage = package['homepage']
13 | s.platform = :ios, '13.4'
14 | s.swift_version = '5.4'
15 | s.source = { git: 'https://github.com/digitalartlab/expo-plugin-localization' }
16 | s.static_framework = true
17 |
18 | s.dependency 'ExpoModulesCore'
19 |
20 | # Swift/Objective-C compatibility
21 | s.pod_target_xcconfig = {
22 | 'DEFINES_MODULE' => 'YES',
23 | 'SWIFT_COMPILATION_MODE' => 'wholemodule'
24 | }
25 |
26 | s.source_files = "**/*.{h,m,swift}"
27 | end
28 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | `expo-plugin-localization` example app
2 | ======================================
3 |
4 | This is an example app for `expo-plugin-localization`. It shows how to use the plugin and how to configure it.
5 |
6 | ## Run the example app
7 |
8 | You can run the example app in an iOS or Android simulator. Ensure you have the [Expo CLI](https://docs.expo.dev/workflow/expo-cli/) installed, as well as a supported version of Xcode and/or Android Studio.
9 |
10 | ```bash
11 | # Run on iOS
12 | npm run ios
13 |
14 | # Run on Android
15 | npm run android
16 | ```
17 |
18 | ## Developing the plugin with the example app
19 |
20 | The example app provides a nice way to test the plugin while developing. It always uses the most recently built version of the plugin in this repository. So just run `npm run build` and `npm run build plugin` in the root of this repository to build the plugin and the config plugin.
21 |
22 | Don't forget to rebuild the native parts of the example app when you change the native code of the plugin. You can do this by running `npx expo prebuild` in the `/example` folder.
23 |
--------------------------------------------------------------------------------
/plugin/src/withIosLocalizableProject.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withXcodeProject } from "expo/config-plugins";
2 |
3 | /**
4 | * Adds a Localizable.strings file reference to the Xcode project for each locale. This is necessary for Xcode to recognize the various languages.
5 | */
6 | export const withIosLocalizableProject: ConfigPlugin<{
7 | locales: string[];
8 | }> = (config, { locales }) => {
9 | return withXcodeProject(config, async (config) => {
10 | const xcodeProject = config.modResults;
11 | locales.forEach((locale) => {
12 | // Add the locale to the project
13 | // Deduplication is handled by the function
14 | xcodeProject.addKnownRegion(locale);
15 | });
16 |
17 | xcodeProject.addPbxGroup("Resources", "Resources");
18 |
19 | const localizationVariantGp = xcodeProject.addLocalizationVariantGroup(
20 | "Localizable.strings",
21 | );
22 | const localizationVariantGpKey = localizationVariantGp.fileRef;
23 |
24 | locales.forEach((locale) => {
25 | // Create a file reference for each locale
26 | xcodeProject.addResourceFile(
27 | `Resources/${locale}.lproj/Localizable.strings`,
28 | { variantGroup: true },
29 | localizationVariantGpKey,
30 | );
31 | });
32 | return config;
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/plugin/src/withAndroidLocalizableResources.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withDangerousMod } from "expo/config-plugins";
2 | import * as fs from "fs";
3 | import * as path from "path";
4 |
5 | /**
6 | * Create res/xml/locales_config.xml file with selected locales
7 | *
8 | * See https://developer.android.com/guide/topics/resources/app-languages#use-localeconfig
9 | */
10 | export const withAndroidLocalizableResources: ConfigPlugin<{
11 | locales: string[];
12 | }> = (config, { locales }) => {
13 | return withDangerousMod(config, [
14 | "android",
15 | async (config) => {
16 | const projectRootPath = path.join(config.modRequest.platformProjectRoot);
17 | const RESOURCES = "app/src/main/res/xml";
18 |
19 | const destAlreadyExists = fs.existsSync(
20 | path.join(projectRootPath, RESOURCES),
21 | );
22 |
23 | if (!destAlreadyExists) {
24 | fs.mkdirSync(path.join(projectRootPath, RESOURCES), {
25 | recursive: true,
26 | });
27 | }
28 |
29 | const localeStrings = locales
30 | .map((locale) => ``)
31 | .join("\n");
32 |
33 | const xml = `
34 |
35 | ${localeStrings}
36 | `;
37 |
38 | fs.writeFileSync(
39 | path.join(projectRootPath, RESOURCES, "locales_config.xml"),
40 | xml,
41 | );
42 |
43 | return config;
44 | },
45 | ]);
46 | };
47 |
--------------------------------------------------------------------------------
/plugin/src/withIosLocalizableResources.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withDangerousMod } from "expo/config-plugins";
2 | import * as fs from "fs";
3 | import * as path from "path";
4 |
5 | /**
6 | * Adds the actual Localizable.strings files to the iOS project folder. These files are empty and are only used to satisfy Xcode.
7 | * This is a dangerous mod because it writes to the file system.
8 | */
9 | export const withIosLocalizableResources: ConfigPlugin<{
10 | locales: string[];
11 | }> = (config, { locales }) => {
12 | return withDangerousMod(config, [
13 | "ios",
14 | (config) => {
15 | const projectRootPath = path.join(config.modRequest.platformProjectRoot);
16 | const RESOURCES = "Resources";
17 |
18 | const destAlreadyExists = fs.existsSync(
19 | path.join(projectRootPath, RESOURCES),
20 | );
21 |
22 | if (!destAlreadyExists) {
23 | fs.mkdirSync(path.join(projectRootPath, RESOURCES));
24 | }
25 |
26 | locales.forEach((locale) => {
27 | const destPath = path.join(
28 | projectRootPath,
29 | RESOURCES,
30 | `${locale}.lproj`,
31 | );
32 |
33 | const destAlreadyExists = fs.existsSync(destPath);
34 |
35 | if (!destAlreadyExists) {
36 | fs.mkdirSync(destPath);
37 | }
38 |
39 | fs.writeFileSync(
40 | path.join(destPath, "Localizable.strings"),
41 | `/* ${locale} */`,
42 | );
43 | });
44 |
45 | return config;
46 | },
47 | ]);
48 | };
49 |
--------------------------------------------------------------------------------
/plugin/src/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | withInfoPlist,
3 | withAndroidManifest,
4 | AndroidConfig,
5 | ConfigPlugin,
6 | } from "expo/config-plugins";
7 |
8 | import { withAndroidLocalizableGradle } from "./withAndroidLocalizableGradle";
9 | import { withAndroidLocalizableManifest } from "./withAndroidLocalizableManifest";
10 | import { withAndroidLocalizableResources } from "./withAndroidLocalizableResources";
11 | import { withIosLocalizableProject } from "./withIosLocalizableProject";
12 | import { withIosLocalizableResources } from "./withIosLocalizableResources";
13 |
14 | const withNativeLocaleSwitching: ConfigPlugin<{ locales?: string[] }> = (
15 | config,
16 | { locales = ["en"] },
17 | ) => {
18 | config = withInfoPlist(config, (config) => {
19 | config.modResults["LOCALES_SUPPORTED"] = locales.join(",");
20 | return config;
21 | });
22 |
23 | config = withAndroidManifest(config, (config) => {
24 | const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(
25 | config.modResults,
26 | );
27 |
28 | AndroidConfig.Manifest.addMetaDataItemToMainApplication(
29 | mainApplication,
30 | "LOCALES_SUPPORTED",
31 | locales.join(","),
32 | );
33 | return config;
34 | });
35 |
36 | config = withIosLocalizableProject(config, { locales });
37 | config = withIosLocalizableResources(config, { locales });
38 | config = withAndroidLocalizableGradle(config, { locales });
39 | config = withAndroidLocalizableManifest(config);
40 | config = withAndroidLocalizableResources(config, { locales });
41 |
42 | return config;
43 | };
44 |
45 | export default withNativeLocaleSwitching;
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@digitalartlab/expo-plugin-localization",
3 | "version": "3.0.0",
4 | "main": "build/index.js",
5 | "types": "build/index.d.ts",
6 | "author": "Digital Art Lab (https://digitalartlab.nl)",
7 | "license": "LGPL-3.0-or-later",
8 | "description": "Native language switching in your Expo app",
9 | "scripts": {
10 | "build": "expo-module build",
11 | "clean": "expo-module clean",
12 | "lint": "expo-module lint",
13 | "test": "expo-module test",
14 | "prepare": "husky",
15 | "prepublishOnly": "expo-module prepublishOnly",
16 | "expo-module": "expo-module",
17 | "open:ios": "open -a \"Xcode\" example/ios",
18 | "open:android": "open -a \"Android Studio\" example/android",
19 | "prepack": "pinst --disable",
20 | "postpack": "pinst --enable"
21 | },
22 | "keywords": [
23 | "expo",
24 | "expo-plugin",
25 | "expo-localization",
26 | "localization",
27 | "i18n",
28 | "internationalization"
29 | ],
30 | "homepage": "https://github.com/digitalartlab/expo-plugin-localization#readme",
31 | "repository": {
32 | "type": "git",
33 | "url": "git+https://github.com/digitalartlab/expo-plugin-localization.git"
34 | },
35 | "bugs": {
36 | "url": "https://github.com/digitalartlab/expo-plugin-localization/issues"
37 | },
38 | "devDependencies": {
39 | "@commitlint/cli": "^18.4.3",
40 | "@commitlint/config-conventional": "^18.4.3",
41 | "@types/fs-extra": "^11.0.4",
42 | "@types/react": "^18.0.25",
43 | "eslint": "^8.56.0",
44 | "expo": "^51.0.24",
45 | "expo-module-scripts": "^3.0.11",
46 | "expo-modules-core": "^1.5.11",
47 | "husky": "^9.1.4",
48 | "pinst": "^3.0.0"
49 | },
50 | "peerDependencies": {
51 | "expo": ">=51.0.0"
52 | },
53 | "peerDependenciesMeta": {
54 | "expo": {
55 | "optional": true
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/plugin/src/withAndroidLocalizableGradle.ts:
--------------------------------------------------------------------------------
1 | import { ConfigPlugin, withAppBuildGradle } from "expo/config-plugins";
2 |
3 | const setAndroidGradleLocalization = (
4 | buildGradle: string,
5 | locales: string[],
6 | ) => {
7 | const localesString = locales.map((locale) => `"${locale}"`).join(", ");
8 |
9 | const resourceConfigurationsString = `resourceConfigurations += [${localesString}]`;
10 |
11 | // There's already an exact match for the resourceConfigurations, so no need to add it again
12 | if (buildGradle.includes(resourceConfigurationsString)) {
13 | return buildGradle;
14 | }
15 |
16 | // There's already a resourceConfigurations, but it's not an exact
17 | // One day, there might be a cleaner way to do this, but for now, we'll just throw an error and force the user to run expo prebuild --clean
18 | if (buildGradle.includes("resourceConfigurations")) {
19 | throw new Error(
20 | `build.gradle already contains a conflicting resourceConfigurations. Please run expo prebuild with the --clean flag to resolve.`,
21 | );
22 | }
23 |
24 | // Add the resourceConfigurations to the defaultConfig
25 | // Mind the indentation
26 | return buildGradle.replace(
27 | /defaultConfig\s*{/,
28 | `defaultConfig {
29 | resourceConfigurations += [${localesString}]`,
30 | );
31 | };
32 |
33 | /**
34 | * Adds the resourceConfigurations with selected locales to the defaultConfig in the build.gradle file
35 | * See https://developer.android.com/guide/topics/resources/app-languages#gradle-config
36 | */
37 | export const withAndroidLocalizableGradle: ConfigPlugin<{
38 | locales: string[];
39 | }> = (config, { locales }) => {
40 | return withAppBuildGradle(config, (config) => {
41 | if (config.modResults.language === "groovy") {
42 | config.modResults.contents = setAndroidGradleLocalization(
43 | config.modResults.contents,
44 | locales,
45 | );
46 | } else {
47 | throw new Error(
48 | `Cannot configure localization because the build.gradle is not groovy`,
49 | );
50 | }
51 |
52 | return config;
53 | });
54 | };
55 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'maven-publish'
4 |
5 | group = 'expo.modules.pluginlocalization'
6 | version = '0.1.0'
7 |
8 | buildscript {
9 | def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
10 | if (expoModulesCorePlugin.exists()) {
11 | apply from: expoModulesCorePlugin
12 | applyKotlinExpoModulesCorePlugin()
13 | }
14 |
15 | // Simple helper that allows the root project to override versions declared by this library.
16 | ext.safeExtGet = { prop, fallback ->
17 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
18 | }
19 |
20 | // Ensures backward compatibility
21 | ext.getKotlinVersion = {
22 | if (ext.has("kotlinVersion")) {
23 | ext.kotlinVersion()
24 | } else {
25 | ext.safeExtGet("kotlinVersion", "1.8.10")
26 | }
27 | }
28 |
29 | repositories {
30 | mavenCentral()
31 | }
32 |
33 | dependencies {
34 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
35 | }
36 | }
37 |
38 | afterEvaluate {
39 | publishing {
40 | publications {
41 | release(MavenPublication) {
42 | from components.release
43 | }
44 | }
45 | repositories {
46 | maven {
47 | url = mavenLocal().url
48 | }
49 | }
50 | }
51 | }
52 |
53 | android {
54 | compileSdkVersion safeExtGet("compileSdkVersion", 33)
55 |
56 | def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
57 | if (agpVersion.tokenize('.')[0].toInteger() < 8) {
58 | compileOptions {
59 | sourceCompatibility JavaVersion.VERSION_11
60 | targetCompatibility JavaVersion.VERSION_11
61 | }
62 |
63 | kotlinOptions {
64 | jvmTarget = JavaVersion.VERSION_11.majorVersion
65 | }
66 | }
67 |
68 | namespace "expo.modules.pluginlocalization"
69 | defaultConfig {
70 | minSdkVersion safeExtGet("minSdkVersion", 21)
71 | targetSdkVersion safeExtGet("targetSdkVersion", 34)
72 | versionCode 1
73 | versionName "0.1.0"
74 | }
75 | lintOptions {
76 | abortOnError false
77 | }
78 | publishing {
79 | singleVariant("release") {
80 | withSourcesJar()
81 | }
82 | }
83 | }
84 |
85 | repositories {
86 | mavenCentral()
87 | }
88 |
89 | dependencies {
90 | implementation project(':expo-modules-core')
91 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
92 | }
93 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [3.0.0](https://github.com/digitalartlab/expo-plugin-localization/compare/v2.0.0...v3.0.0) (2024-07-31)
4 |
5 |
6 | ### ⚠ BREAKING CHANGES
7 |
8 | * requires Expo SDK 51
9 |
10 | ### Features
11 |
12 | * upgrade for Expo 51 ([7f5c52a](https://github.com/digitalartlab/expo-plugin-localization/commit/7f5c52aa6aa1c021eca3ead41e024d9962f08b23))
13 |
14 |
15 | ### Bug Fixes
16 |
17 | * husky blocked package installs ([2faa8b1](https://github.com/digitalartlab/expo-plugin-localization/commit/2faa8b18ef69a9f792061448a9449cd770ad58e9))
18 |
19 |
20 | ### Documentation
21 |
22 | * update Security Policy for major release ([96b4618](https://github.com/digitalartlab/expo-plugin-localization/commit/96b4618d17d6a3f4ce2ab6040cc3aca33b4a8b90))
23 |
24 | ## [2.0.0](https://github.com/digitalartlab/expo-plugin-localization/compare/v1.1.1...v2.0.0) (2024-01-26)
25 |
26 |
27 | ### ⚠ BREAKING CHANGES
28 |
29 | * requires Expo SDK 50 or newer
30 | * change build.gradle and podspec files for compatibility with Expo SDK 50
31 |
32 | ### Bug Fixes
33 |
34 | * change build.gradle and podspec files for compatibility with Expo SDK 50 ([2c50897](https://github.com/digitalartlab/expo-plugin-localization/commit/2c50897175839bc5006d74861a1c777a8a08156f))
35 |
36 |
37 | ### Documentation
38 |
39 | * update SECURITY for v2.0.0 ([cf37fc4](https://github.com/digitalartlab/expo-plugin-localization/commit/cf37fc4cdd88ef870f46a30827b931bcadccecdc))
40 |
41 | ## [1.1.1](https://github.com/digitalartlab/expo-plugin-localization/compare/v1.1.0...v1.1.1) (2023-12-23)
42 |
43 |
44 | ### Bug Fixes
45 |
46 | * can't build app due to wrong class names in native code ([8bdad3d](https://github.com/digitalartlab/expo-plugin-localization/commit/8bdad3deaebe21417d51fed6108683a89a7a30d3))
47 |
48 | ## [1.1.0](https://github.com/digitalartlab/expo-plugin-localization/compare/v1.0.0...v1.1.0) (2023-12-23)
49 |
50 |
51 | ### Features
52 |
53 | * get currently supported locales in app ([29a7310](https://github.com/digitalartlab/expo-plugin-localization/commit/29a731035110b99760d9dcb4c5ada7b019db022e))
54 |
55 | ## [1.0.0](https://github.com/digitalartlab/expo-plugin-localization/compare/v0.1.3...v1.0.0) (2023-04-23)
56 |
57 |
58 | ### ⚠ BREAKING CHANGES
59 |
60 | * rename `knownRegions` to `locales`
61 |
62 | ### Features
63 |
64 | * add support for per-app language selection on Android 13+ ([eb58d2f](https://github.com/digitalartlab/expo-plugin-localization/commit/eb58d2fad8942129919d25f8e6e1c8d27f6ef5fa))
65 |
66 | ## [0.1.3](https://github.com/digitalartlab/expo-plugin-localization/compare/v0.1.2...v0.1.3) (2023-04-23)
67 |
68 |
69 | ### Performance Improvements
70 |
71 | * make NPM package smaller ([1885350](https://github.com/digitalartlab/expo-plugin-localization/commit/18853507e503a0ea5f6ab9b8a75c26bf5ee266b5))
72 |
73 | ## [0.1.2](https://github.com/digitalartlab/expo-plugin-localization/compare/v0.1.1...v0.1.2) (2023-04-15)
74 |
75 |
76 | ### Bug Fixes
77 |
78 | * set up Yarn for package publishing ([b03e481](https://github.com/digitalartlab/expo-plugin-localization/commit/b03e4811db635b53e6c1b67d7e914b1714911cc7))
79 |
80 | ## [0.1.1](https://github.com/digitalartlab/expo-plugin-localization/compare/v0.1.0...v0.1.1) (2023-04-15)
81 |
82 |
83 | ### Bug Fixes
84 |
85 | * husky runs on package install ([d6658ba](https://github.com/digitalartlab/expo-plugin-localization/commit/d6658ba861a3494150429f64c992a5560f76e2cc))
86 |
87 | ## 0.1.0 (2023-04-15)
88 |
89 |
90 | ### Features
91 |
92 | * initial commit ([7bbeac5](https://github.com/digitalartlab/expo-plugin-localization/commit/7bbeac511abb8dbfded06023a42a2879b53a72ff))
93 |
94 |
95 | ### Documentation
96 |
97 | * add explanation on what the plugin does and how it works ([9f71cb5](https://github.com/digitalartlab/expo-plugin-localization/commit/9f71cb56920a1dd00c65979b21cf6ac8c636c810))
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Expo Plugin Localization
2 |
3 | [](https://badge.fury.io/js/%40digitalartlab%2Fexpo-plugin-localization)
4 | [](https://www.gnu.org/licenses/lgpl-3.0)
5 |
6 | Per-app language switching on iOS and Android through the Settings app using the OS's native language switching functionalities for your Expo app.
7 |
8 | Expo's [Localization](https://docs.expo.io/versions/latest/sdk/localization/) module is great, but it doesn't support native language switching on iOS through the Settings app. That might result in you displaying the wrong language to the user. Ever tried to order plane tickets in Spanish because the app decided you totally spoke it? Yeah, not fun.
9 |
10 | This plugin creates the necessary files and references in your Xcode and Android Studio projects to support native language switching on iOS and Android. It does not handle the actual translations or fetching the locale in your app. You can use any library you want for that, like [i18next](https://www.i18next.com/) for the translation and [expo-localization](https://docs.expo.io/versions/latest/sdk/localization/) for the locale detection.
11 |
12 | ## Installation
13 |
14 | Install the package with the Expo CLI:
15 |
16 | ```bash
17 | npx expo install @digitalartlab/expo-plugin-localization
18 | ```
19 |
20 | Then add the plugin to your `app.json`:
21 |
22 | ```json
23 | {
24 | "expo": {
25 | "plugins": ["@digitalartlab/expo-plugin-localization"]
26 | }
27 | }
28 | ```
29 |
30 | ## Configuration
31 |
32 | You provide an array of locales that you want to support. The default value is `["en"]`.
33 |
34 | ```json
35 | {
36 | "expo": {
37 | "plugins": [
38 | [
39 | "@digitalartlab/expo-plugin-localization",
40 | {
41 | "locales": ["en", "nl"]
42 | }
43 | ]
44 | ]
45 | }
46 | }
47 | ```
48 |
49 | ### Supported languages
50 | This plugin supports all the two-letter language codes (`nl`, `en`, `es`, etc.) Android and iOS support.
51 |
52 | Language codes with region and script affixes (`nl-NL`, `en-GB`, `zh-hans-CN`, etc.) are _not_ supported. This is because Android requires different formatting for language codes in various config files. If you _really_ need this, feel free to open an issue or a pull request.
53 |
54 | ### Prebuild
55 |
56 | If you use [prebuild](https://docs.expo.dev/workflow/prebuild/), you have to use the `--clean` flag every time you change the config of this plugin. This is because the plugin can't reliably purge the old values from the config files. And you don't want a language to linger around. That's like Duolingo reminding you to practise long after you gave up!
57 |
58 | Not using prebuild? You're good to go!
59 |
60 | ## Usage
61 |
62 | Expo has a great [Localization guide](https://docs.expo.dev/guides/localization/) that explains how to fetch the user's current locale. This plugin doesn't change that, so you can use the same code. Behind the scenes, the OS sorts the list of locales to match the user's selection in the Settings app. So, if a user's phone is set to Dutch and the app is set to English, the list of locales will be `["en", "nl"]` instead of `["nl", "en"]`.
63 |
64 | Basically: do what you'd already do to detect the user's language and just pick the first value you get.
65 |
66 | If you want to explicitely get the list of locales you configured, you can use the `getLocales` method. It returns an array of strings with the locales you configured.
67 |
68 | ```js
69 | import { getLocales } from "@digitalartlab/expo-plugin-localization";
70 |
71 | const locales = getLocales();
72 | ```
73 |
74 | > [!IMPORTANT]
75 | > The `getLocales` method returns the locales in the order you configured them. If you want to get the user's preferred locale (to, you know, actually support locale switching), use the Expo [Localization](https://docs.expo.dev/versions/latest/sdk/localization/) module.
76 |
77 | ## Contributing
78 |
79 | Contributions are very welcome! Please take a moment to read our [Code of Conduct](https://github.com/digitalartlab/.github/blob/main/CODE_OF_CONDUCT.md) before contributing.
80 |
81 | ## Development on this plugin
82 |
83 | Run `npm install` to install the dependencies.
84 |
85 | ### Structure
86 |
87 | This project consists of two parts: an Expo module and a config plugin. They work hand in hand.
88 |
89 | First, the config plugin adds the necessary files and references to your Xcode and Android Studio projects to support native language switching. The module then optionally exposes the configured locales to your app, if you don't want to use another module like `expo-localization` for that, or need the exact locales you configured.
90 |
91 | All code for the config plugin is in the `/plugin/src` folder. For the module, the code is split between the `/src` folder for the TypeScript code and the `/ios` and `/android` folders for the native code.
92 |
93 | The `/example` folder contains a barebones Expo app that you can use to test the plugin.
94 |
95 | ### Building
96 |
97 | To build the module, run `npm run build`. To build the config plugin, run `npm run build plugin`.
98 |
99 | ### Commit messages
100 |
101 | This repo uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) to automatically generate changelogs. Please make sure your commit messages follow this format. A Husky pre-commit hook is in place to verify this when you commit.
102 |
103 | ### Publishing
104 |
105 | To publish a new version of the module, first merge the PR automatically created by Release Please. This will bump the version number, update `CHANGELOG.md` and create a new release tag on GitHub. Then, run `npm run publish` to publish the module to NPM. Could this be automated? Yes. Is it? No. Why? ¯\\\_(ツ)\_/¯
106 |
107 | ## License
108 |
109 | This project is licensed under the [LGPL-3.0 license](LICENSE). This means you can freely use it in your own projects, also commercial ones. However, if you make changes to this specific library, you have to share those changes with the community. We would definitely welcome a pull request!
110 |
111 | ## Author
112 |
113 | Thijmen de Valk ([@thijmendevalk](https://github.com/thijmendevalk))
114 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright © 2007 Free Software Foundation, Inc.
5 |
6 | Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
7 |
8 | This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below.
9 |
10 | 0. Additional Definitions.
11 | As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License.
12 |
13 | “The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below.
14 |
15 | An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library.
16 |
17 | A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”.
18 |
19 | The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version.
20 |
21 | The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work.
22 |
23 | 1. Exception to Section 3 of the GNU GPL.
24 | You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL.
25 |
26 | 2. Conveying Modified Versions.
27 | If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version:
28 |
29 | a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or
30 | b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy.
31 | 3. Object Code Incorporating Material from Library Header Files.
32 | The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following:
33 |
34 | a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License.
35 | b) Accompany the object code with a copy of the GNU GPL and this license document.
36 | 4. Combined Works.
37 | You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following:
38 |
39 | a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License.
40 | b) Accompany the Combined Work with a copy of the GNU GPL and this license document.
41 | c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document.
42 | d) Do one of the following:
43 | 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.
44 | 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version.
45 | e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.)
46 | 5. Combined Libraries.
47 | You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following:
48 |
49 | a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License.
50 | b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
51 | 6. Revised Versions of the GNU Lesser General Public License.
52 | The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
53 |
54 | Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation.
55 |
56 | If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library.
--------------------------------------------------------------------------------