map-gl-offline 0.1.0-alpha.7 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -88,7 +88,7 @@ var LogLevel;
88
88
  * Main Logger class providing centralized logging functionality.
89
89
  * @internal Use the exported `logger` singleton or `configureLogger()` function.
90
90
  */
91
- class Logger {
91
+ let Logger$1 = class Logger {
92
92
  level = LogLevel.ERROR; // Default to ERROR only for production
93
93
  /**
94
94
  * Configure the logger settings.
@@ -197,7 +197,7 @@ class Logger {
197
197
  scope(prefix) {
198
198
  return new ScopedLogger(prefix, this);
199
199
  }
200
- }
200
+ };
201
201
  /**
202
202
  * A scoped logger that prefixes all messages with a module/component name.
203
203
  * Created using `logger.scope('PrefixName')`.
@@ -253,7 +253,7 @@ class ScopedLogger {
253
253
  * logger.error('Something went wrong', error);
254
254
  * ```
255
255
  */
256
- const logger = new Logger();
256
+ const logger = new Logger$1();
257
257
  /**
258
258
  * Configure the global logger settings.
259
259
  * Call this early in your application to set logging preferences.
@@ -737,6 +737,7 @@ const TABLER_ICONS = {
737
737
  upload: '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-upload" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2" /><polyline points="7,9 12,4 17,9" /><line x1="12" y1="4" x2="12" y2="16" /></svg>',
738
738
  infoCircle: '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-info-circle" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><circle cx="12" cy="12" r="9" /><line x1="12" y1="8" x2="12.01" y2="8" /><polyline points="11,12 12,12 12,16 13,16" /></svg>',
739
739
  deviceDesktop: '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-device-desktop" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><rect x="3" y="4" width="18" height="12" rx="1" /><line x1="7" y1="20" x2="17" y2="20" /><line x1="9" y1="16" x2="9" y2="20" /><line x1="15" y1="16" x2="15" y2="20" /></svg>',
740
+ language: '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-language" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 5h7" /><path d="M9 3v2c0 4.418 -2.239 8 -5 8" /><path d="M5 9c0 2.144 2.952 3.908 6.7 4" /><path d="M12 20l4 -9l4 9" /><path d="M19.1 18h-6.2" /></svg>',
740
741
  };
741
742
  /**
742
743
  * Default icon configuration
@@ -817,6 +818,8 @@ const icons = {
817
818
  infoCircle: (config) => renderIcon(TABLER_ICONS.infoCircle, config),
818
819
  // System/Device icons
819
820
  deviceDesktop: (config) => renderIcon(TABLER_ICONS.deviceDesktop, config),
821
+ // Language icons
822
+ language: (config) => renderIcon(TABLER_ICONS.language, config),
820
823
  };
821
824
  /**
822
825
  * Helper function to get an icon by name
@@ -6868,155 +6871,2909 @@ async function downloadStyleWithProvider(styleUrl, options = {}) {
6868
6871
  },
6869
6872
  };
6870
6873
  }
6871
- catch (error) {
6872
- logger.error(`Failed to download style from ${styleUrl}:`, error);
6873
- return {
6874
- styleId: '',
6875
- success: false,
6876
- downloadTime: Date.now() - startTime,
6877
- styleSize: 0,
6878
- sourcesProcessed: 0,
6879
- sourcesEmbedded: 0,
6880
- errors: [error instanceof Error ? error.message : String(error)],
6881
- analytics: {
6882
- sourceTypes: {},
6883
- layerTypes: {},
6884
- totalLayers: 0,
6885
- hasGlyphs: false,
6886
- hasSprites: false,
6887
- },
6874
+ catch (error) {
6875
+ logger.error(`Failed to download style from ${styleUrl}:`, error);
6876
+ return {
6877
+ styleId: '',
6878
+ success: false,
6879
+ downloadTime: Date.now() - startTime,
6880
+ styleSize: 0,
6881
+ sourcesProcessed: 0,
6882
+ sourcesEmbedded: 0,
6883
+ errors: [error instanceof Error ? error.message : String(error)],
6884
+ analytics: {
6885
+ sourceTypes: {},
6886
+ layerTypes: {},
6887
+ totalLayers: 0,
6888
+ hasGlyphs: false,
6889
+ hasSprites: false,
6890
+ },
6891
+ };
6892
+ }
6893
+ }
6894
+
6895
+ var styleService = /*#__PURE__*/Object.freeze({
6896
+ __proto__: null,
6897
+ cleanupOldStyles: cleanupOldStyles,
6898
+ deleteStyleById: deleteStyleById,
6899
+ deleteStyles: deleteStyles,
6900
+ downloadStyleWithProvider: downloadStyleWithProvider,
6901
+ downloadStyles: downloadStyles,
6902
+ getStyleStats: getStyleStats,
6903
+ isStyleDownloaded: isStyleDownloaded,
6904
+ loadStyleById: loadStyleById,
6905
+ loadStyles: loadStyles
6906
+ });
6907
+
6908
+ /**
6909
+ * Simplified theme system for dark/light/system mode management
6910
+ * Now that all components use Tailwind CSS, we only need basic theme switching
6911
+ */
6912
+ const lightTheme = {
6913
+ mode: 'light',
6914
+ preference: 'light',
6915
+ };
6916
+ // Theme context management
6917
+ class ThemeManager {
6918
+ currentTheme = lightTheme;
6919
+ listeners = [];
6920
+ constructor() {
6921
+ // Initialize theme from localStorage or system preference
6922
+ this.initializeTheme();
6923
+ }
6924
+ initializeTheme() {
6925
+ const savedPreference = localStorage.getItem('offline-manager-theme');
6926
+ if (savedPreference === 'system' || !savedPreference) {
6927
+ // Use system preference
6928
+ this.setThemePreference('system');
6929
+ }
6930
+ else {
6931
+ this.setThemePreference(savedPreference);
6932
+ }
6933
+ // Listen for system theme changes
6934
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
6935
+ if (this.currentTheme.preference === 'system') {
6936
+ this.applyThemeMode(e.matches ? 'dark' : 'light', 'system');
6937
+ }
6938
+ });
6939
+ }
6940
+ getTheme() {
6941
+ return this.currentTheme;
6942
+ }
6943
+ /**
6944
+ * Get the effective theme mode (light or dark)
6945
+ */
6946
+ getEffectiveMode() {
6947
+ return this.currentTheme.mode;
6948
+ }
6949
+ /**
6950
+ * Get the user's preference (light, dark, or system)
6951
+ */
6952
+ getPreference() {
6953
+ return this.currentTheme.preference;
6954
+ }
6955
+ applyThemeMode(mode, preference) {
6956
+ this.currentTheme = { mode, preference };
6957
+ // Apply theme to document for Tailwind dark mode
6958
+ if (mode === 'dark') {
6959
+ document.documentElement.classList.add('dark');
6960
+ }
6961
+ else {
6962
+ document.documentElement.classList.remove('dark');
6963
+ }
6964
+ this.notifyListeners();
6965
+ }
6966
+ /**
6967
+ * Set the theme preference (light, dark, or system)
6968
+ */
6969
+ setThemePreference(preference) {
6970
+ localStorage.setItem('offline-manager-theme', preference);
6971
+ if (preference === 'system') {
6972
+ const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
6973
+ this.applyThemeMode(systemPrefersDark ? 'dark' : 'light', 'system');
6974
+ }
6975
+ else {
6976
+ this.applyThemeMode(preference, preference);
6977
+ }
6978
+ }
6979
+ /**
6980
+ * Set the theme mode directly (for backwards compatibility)
6981
+ * @deprecated Use setThemePreference instead
6982
+ */
6983
+ setTheme(mode) {
6984
+ this.setThemePreference(mode);
6985
+ }
6986
+ /**
6987
+ * Cycle through themes: light -> dark -> system -> light
6988
+ */
6989
+ cycleTheme() {
6990
+ const preference = this.currentTheme.preference;
6991
+ if (preference === 'light') {
6992
+ this.setThemePreference('dark');
6993
+ }
6994
+ else if (preference === 'dark') {
6995
+ this.setThemePreference('system');
6996
+ }
6997
+ else {
6998
+ this.setThemePreference('light');
6999
+ }
7000
+ }
7001
+ /**
7002
+ * Toggle between light and dark (legacy behavior)
7003
+ * @deprecated Use cycleTheme for light/dark/system cycling
7004
+ */
7005
+ toggleTheme() {
7006
+ this.cycleTheme();
7007
+ }
7008
+ subscribe(listener) {
7009
+ this.listeners.push(listener);
7010
+ return () => {
7011
+ const index = this.listeners.indexOf(listener);
7012
+ if (index > -1) {
7013
+ this.listeners.splice(index, 1);
7014
+ }
7015
+ };
7016
+ }
7017
+ notifyListeners() {
7018
+ this.listeners.forEach(listener => listener(this.currentTheme));
7019
+ }
7020
+ }
7021
+ // Export singleton instance
7022
+ const themeManager = new ThemeManager();
7023
+
7024
+ const isString = obj => typeof obj === 'string';
7025
+ const defer = () => {
7026
+ let res;
7027
+ let rej;
7028
+ const promise = new Promise((resolve, reject) => {
7029
+ res = resolve;
7030
+ rej = reject;
7031
+ });
7032
+ promise.resolve = res;
7033
+ promise.reject = rej;
7034
+ return promise;
7035
+ };
7036
+ const makeString = object => {
7037
+ if (object == null) return '';
7038
+ return '' + object;
7039
+ };
7040
+ const copy = (a, s, t) => {
7041
+ a.forEach(m => {
7042
+ if (s[m]) t[m] = s[m];
7043
+ });
7044
+ };
7045
+ const lastOfPathSeparatorRegExp = /###/g;
7046
+ const cleanKey = key => key && key.indexOf('###') > -1 ? key.replace(lastOfPathSeparatorRegExp, '.') : key;
7047
+ const canNotTraverseDeeper = object => !object || isString(object);
7048
+ const getLastOfPath = (object, path, Empty) => {
7049
+ const stack = !isString(path) ? path : path.split('.');
7050
+ let stackIndex = 0;
7051
+ while (stackIndex < stack.length - 1) {
7052
+ if (canNotTraverseDeeper(object)) return {};
7053
+ const key = cleanKey(stack[stackIndex]);
7054
+ if (!object[key] && Empty) object[key] = new Empty();
7055
+ if (Object.prototype.hasOwnProperty.call(object, key)) {
7056
+ object = object[key];
7057
+ } else {
7058
+ object = {};
7059
+ }
7060
+ ++stackIndex;
7061
+ }
7062
+ if (canNotTraverseDeeper(object)) return {};
7063
+ return {
7064
+ obj: object,
7065
+ k: cleanKey(stack[stackIndex])
7066
+ };
7067
+ };
7068
+ const setPath = (object, path, newValue) => {
7069
+ const {
7070
+ obj,
7071
+ k
7072
+ } = getLastOfPath(object, path, Object);
7073
+ if (obj !== undefined || path.length === 1) {
7074
+ obj[k] = newValue;
7075
+ return;
7076
+ }
7077
+ let e = path[path.length - 1];
7078
+ let p = path.slice(0, path.length - 1);
7079
+ let last = getLastOfPath(object, p, Object);
7080
+ while (last.obj === undefined && p.length) {
7081
+ e = `${p[p.length - 1]}.${e}`;
7082
+ p = p.slice(0, p.length - 1);
7083
+ last = getLastOfPath(object, p, Object);
7084
+ if (last?.obj && typeof last.obj[`${last.k}.${e}`] !== 'undefined') {
7085
+ last.obj = undefined;
7086
+ }
7087
+ }
7088
+ last.obj[`${last.k}.${e}`] = newValue;
7089
+ };
7090
+ const pushPath = (object, path, newValue, concat) => {
7091
+ const {
7092
+ obj,
7093
+ k
7094
+ } = getLastOfPath(object, path, Object);
7095
+ obj[k] = obj[k] || [];
7096
+ obj[k].push(newValue);
7097
+ };
7098
+ const getPath = (object, path) => {
7099
+ const {
7100
+ obj,
7101
+ k
7102
+ } = getLastOfPath(object, path);
7103
+ if (!obj) return undefined;
7104
+ if (!Object.prototype.hasOwnProperty.call(obj, k)) return undefined;
7105
+ return obj[k];
7106
+ };
7107
+ const getPathWithDefaults = (data, defaultData, key) => {
7108
+ const value = getPath(data, key);
7109
+ if (value !== undefined) {
7110
+ return value;
7111
+ }
7112
+ return getPath(defaultData, key);
7113
+ };
7114
+ const deepExtend = (target, source, overwrite) => {
7115
+ for (const prop in source) {
7116
+ if (prop !== '__proto__' && prop !== 'constructor') {
7117
+ if (prop in target) {
7118
+ if (isString(target[prop]) || target[prop] instanceof String || isString(source[prop]) || source[prop] instanceof String) {
7119
+ if (overwrite) target[prop] = source[prop];
7120
+ } else {
7121
+ deepExtend(target[prop], source[prop], overwrite);
7122
+ }
7123
+ } else {
7124
+ target[prop] = source[prop];
7125
+ }
7126
+ }
7127
+ }
7128
+ return target;
7129
+ };
7130
+ const regexEscape = str => str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
7131
+ var _entityMap = {
7132
+ '&': '&amp;',
7133
+ '<': '&lt;',
7134
+ '>': '&gt;',
7135
+ '"': '&quot;',
7136
+ "'": '&#39;',
7137
+ '/': '&#x2F;'
7138
+ };
7139
+ const escape = data => {
7140
+ if (isString(data)) {
7141
+ return data.replace(/[&<>"'\/]/g, s => _entityMap[s]);
7142
+ }
7143
+ return data;
7144
+ };
7145
+ class RegExpCache {
7146
+ constructor(capacity) {
7147
+ this.capacity = capacity;
7148
+ this.regExpMap = new Map();
7149
+ this.regExpQueue = [];
7150
+ }
7151
+ getRegExp(pattern) {
7152
+ const regExpFromCache = this.regExpMap.get(pattern);
7153
+ if (regExpFromCache !== undefined) {
7154
+ return regExpFromCache;
7155
+ }
7156
+ const regExpNew = new RegExp(pattern);
7157
+ if (this.regExpQueue.length === this.capacity) {
7158
+ this.regExpMap.delete(this.regExpQueue.shift());
7159
+ }
7160
+ this.regExpMap.set(pattern, regExpNew);
7161
+ this.regExpQueue.push(pattern);
7162
+ return regExpNew;
7163
+ }
7164
+ }
7165
+ const chars = [' ', ',', '?', '!', ';'];
7166
+ const looksLikeObjectPathRegExpCache = new RegExpCache(20);
7167
+ const looksLikeObjectPath = (key, nsSeparator, keySeparator) => {
7168
+ nsSeparator = nsSeparator || '';
7169
+ keySeparator = keySeparator || '';
7170
+ const possibleChars = chars.filter(c => nsSeparator.indexOf(c) < 0 && keySeparator.indexOf(c) < 0);
7171
+ if (possibleChars.length === 0) return true;
7172
+ const r = looksLikeObjectPathRegExpCache.getRegExp(`(${possibleChars.map(c => c === '?' ? '\\?' : c).join('|')})`);
7173
+ let matched = !r.test(key);
7174
+ if (!matched) {
7175
+ const ki = key.indexOf(keySeparator);
7176
+ if (ki > 0 && !r.test(key.substring(0, ki))) {
7177
+ matched = true;
7178
+ }
7179
+ }
7180
+ return matched;
7181
+ };
7182
+ const deepFind = (obj, path, keySeparator = '.') => {
7183
+ if (!obj) return undefined;
7184
+ if (obj[path]) {
7185
+ if (!Object.prototype.hasOwnProperty.call(obj, path)) return undefined;
7186
+ return obj[path];
7187
+ }
7188
+ const tokens = path.split(keySeparator);
7189
+ let current = obj;
7190
+ for (let i = 0; i < tokens.length;) {
7191
+ if (!current || typeof current !== 'object') {
7192
+ return undefined;
7193
+ }
7194
+ let next;
7195
+ let nextPath = '';
7196
+ for (let j = i; j < tokens.length; ++j) {
7197
+ if (j !== i) {
7198
+ nextPath += keySeparator;
7199
+ }
7200
+ nextPath += tokens[j];
7201
+ next = current[nextPath];
7202
+ if (next !== undefined) {
7203
+ if (['string', 'number', 'boolean'].indexOf(typeof next) > -1 && j < tokens.length - 1) {
7204
+ continue;
7205
+ }
7206
+ i += j - i + 1;
7207
+ break;
7208
+ }
7209
+ }
7210
+ current = next;
7211
+ }
7212
+ return current;
7213
+ };
7214
+ const getCleanedCode = code => code?.replace('_', '-');
7215
+
7216
+ const consoleLogger = {
7217
+ type: 'logger',
7218
+ log(args) {
7219
+ this.output('log', args);
7220
+ },
7221
+ warn(args) {
7222
+ this.output('warn', args);
7223
+ },
7224
+ error(args) {
7225
+ this.output('error', args);
7226
+ },
7227
+ output(type, args) {
7228
+ console?.[type]?.apply?.(console, args);
7229
+ }
7230
+ };
7231
+ class Logger {
7232
+ constructor(concreteLogger, options = {}) {
7233
+ this.init(concreteLogger, options);
7234
+ }
7235
+ init(concreteLogger, options = {}) {
7236
+ this.prefix = options.prefix || 'i18next:';
7237
+ this.logger = concreteLogger || consoleLogger;
7238
+ this.options = options;
7239
+ this.debug = options.debug;
7240
+ }
7241
+ log(...args) {
7242
+ return this.forward(args, 'log', '', true);
7243
+ }
7244
+ warn(...args) {
7245
+ return this.forward(args, 'warn', '', true);
7246
+ }
7247
+ error(...args) {
7248
+ return this.forward(args, 'error', '');
7249
+ }
7250
+ deprecate(...args) {
7251
+ return this.forward(args, 'warn', 'WARNING DEPRECATED: ', true);
7252
+ }
7253
+ forward(args, lvl, prefix, debugOnly) {
7254
+ if (debugOnly && !this.debug) return null;
7255
+ if (isString(args[0])) args[0] = `${prefix}${this.prefix} ${args[0]}`;
7256
+ return this.logger[lvl](args);
7257
+ }
7258
+ create(moduleName) {
7259
+ return new Logger(this.logger, {
7260
+ ...{
7261
+ prefix: `${this.prefix}:${moduleName}:`
7262
+ },
7263
+ ...this.options
7264
+ });
7265
+ }
7266
+ clone(options) {
7267
+ options = options || this.options;
7268
+ options.prefix = options.prefix || this.prefix;
7269
+ return new Logger(this.logger, options);
7270
+ }
7271
+ }
7272
+ var baseLogger = new Logger();
7273
+
7274
+ class EventEmitter {
7275
+ constructor() {
7276
+ this.observers = {};
7277
+ }
7278
+ on(events, listener) {
7279
+ events.split(' ').forEach(event => {
7280
+ if (!this.observers[event]) this.observers[event] = new Map();
7281
+ const numListeners = this.observers[event].get(listener) || 0;
7282
+ this.observers[event].set(listener, numListeners + 1);
7283
+ });
7284
+ return this;
7285
+ }
7286
+ off(event, listener) {
7287
+ if (!this.observers[event]) return;
7288
+ if (!listener) {
7289
+ delete this.observers[event];
7290
+ return;
7291
+ }
7292
+ this.observers[event].delete(listener);
7293
+ }
7294
+ emit(event, ...args) {
7295
+ if (this.observers[event]) {
7296
+ const cloned = Array.from(this.observers[event].entries());
7297
+ cloned.forEach(([observer, numTimesAdded]) => {
7298
+ for (let i = 0; i < numTimesAdded; i++) {
7299
+ observer(...args);
7300
+ }
7301
+ });
7302
+ }
7303
+ if (this.observers['*']) {
7304
+ const cloned = Array.from(this.observers['*'].entries());
7305
+ cloned.forEach(([observer, numTimesAdded]) => {
7306
+ for (let i = 0; i < numTimesAdded; i++) {
7307
+ observer.apply(observer, [event, ...args]);
7308
+ }
7309
+ });
7310
+ }
7311
+ }
7312
+ }
7313
+
7314
+ class ResourceStore extends EventEmitter {
7315
+ constructor(data, options = {
7316
+ ns: ['translation'],
7317
+ defaultNS: 'translation'
7318
+ }) {
7319
+ super();
7320
+ this.data = data || {};
7321
+ this.options = options;
7322
+ if (this.options.keySeparator === undefined) {
7323
+ this.options.keySeparator = '.';
7324
+ }
7325
+ if (this.options.ignoreJSONStructure === undefined) {
7326
+ this.options.ignoreJSONStructure = true;
7327
+ }
7328
+ }
7329
+ addNamespaces(ns) {
7330
+ if (this.options.ns.indexOf(ns) < 0) {
7331
+ this.options.ns.push(ns);
7332
+ }
7333
+ }
7334
+ removeNamespaces(ns) {
7335
+ const index = this.options.ns.indexOf(ns);
7336
+ if (index > -1) {
7337
+ this.options.ns.splice(index, 1);
7338
+ }
7339
+ }
7340
+ getResource(lng, ns, key, options = {}) {
7341
+ const keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator;
7342
+ const ignoreJSONStructure = options.ignoreJSONStructure !== undefined ? options.ignoreJSONStructure : this.options.ignoreJSONStructure;
7343
+ let path;
7344
+ if (lng.indexOf('.') > -1) {
7345
+ path = lng.split('.');
7346
+ } else {
7347
+ path = [lng, ns];
7348
+ if (key) {
7349
+ if (Array.isArray(key)) {
7350
+ path.push(...key);
7351
+ } else if (isString(key) && keySeparator) {
7352
+ path.push(...key.split(keySeparator));
7353
+ } else {
7354
+ path.push(key);
7355
+ }
7356
+ }
7357
+ }
7358
+ const result = getPath(this.data, path);
7359
+ if (!result && !ns && !key && lng.indexOf('.') > -1) {
7360
+ lng = path[0];
7361
+ ns = path[1];
7362
+ key = path.slice(2).join('.');
7363
+ }
7364
+ if (result || !ignoreJSONStructure || !isString(key)) return result;
7365
+ return deepFind(this.data?.[lng]?.[ns], key, keySeparator);
7366
+ }
7367
+ addResource(lng, ns, key, value, options = {
7368
+ silent: false
7369
+ }) {
7370
+ const keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator;
7371
+ let path = [lng, ns];
7372
+ if (key) path = path.concat(keySeparator ? key.split(keySeparator) : key);
7373
+ if (lng.indexOf('.') > -1) {
7374
+ path = lng.split('.');
7375
+ value = ns;
7376
+ ns = path[1];
7377
+ }
7378
+ this.addNamespaces(ns);
7379
+ setPath(this.data, path, value);
7380
+ if (!options.silent) this.emit('added', lng, ns, key, value);
7381
+ }
7382
+ addResources(lng, ns, resources, options = {
7383
+ silent: false
7384
+ }) {
7385
+ for (const m in resources) {
7386
+ if (isString(resources[m]) || Array.isArray(resources[m])) this.addResource(lng, ns, m, resources[m], {
7387
+ silent: true
7388
+ });
7389
+ }
7390
+ if (!options.silent) this.emit('added', lng, ns, resources);
7391
+ }
7392
+ addResourceBundle(lng, ns, resources, deep, overwrite, options = {
7393
+ silent: false,
7394
+ skipCopy: false
7395
+ }) {
7396
+ let path = [lng, ns];
7397
+ if (lng.indexOf('.') > -1) {
7398
+ path = lng.split('.');
7399
+ deep = resources;
7400
+ resources = ns;
7401
+ ns = path[1];
7402
+ }
7403
+ this.addNamespaces(ns);
7404
+ let pack = getPath(this.data, path) || {};
7405
+ if (!options.skipCopy) resources = JSON.parse(JSON.stringify(resources));
7406
+ if (deep) {
7407
+ deepExtend(pack, resources, overwrite);
7408
+ } else {
7409
+ pack = {
7410
+ ...pack,
7411
+ ...resources
7412
+ };
7413
+ }
7414
+ setPath(this.data, path, pack);
7415
+ if (!options.silent) this.emit('added', lng, ns, resources);
7416
+ }
7417
+ removeResourceBundle(lng, ns) {
7418
+ if (this.hasResourceBundle(lng, ns)) {
7419
+ delete this.data[lng][ns];
7420
+ }
7421
+ this.removeNamespaces(ns);
7422
+ this.emit('removed', lng, ns);
7423
+ }
7424
+ hasResourceBundle(lng, ns) {
7425
+ return this.getResource(lng, ns) !== undefined;
7426
+ }
7427
+ getResourceBundle(lng, ns) {
7428
+ if (!ns) ns = this.options.defaultNS;
7429
+ return this.getResource(lng, ns);
7430
+ }
7431
+ getDataByLanguage(lng) {
7432
+ return this.data[lng];
7433
+ }
7434
+ hasLanguageSomeTranslations(lng) {
7435
+ const data = this.getDataByLanguage(lng);
7436
+ const n = data && Object.keys(data) || [];
7437
+ return !!n.find(v => data[v] && Object.keys(data[v]).length > 0);
7438
+ }
7439
+ toJSON() {
7440
+ return this.data;
7441
+ }
7442
+ }
7443
+
7444
+ var postProcessor = {
7445
+ processors: {},
7446
+ addPostProcessor(module) {
7447
+ this.processors[module.name] = module;
7448
+ },
7449
+ handle(processors, value, key, options, translator) {
7450
+ processors.forEach(processor => {
7451
+ value = this.processors[processor]?.process(value, key, options, translator) ?? value;
7452
+ });
7453
+ return value;
7454
+ }
7455
+ };
7456
+
7457
+ const PATH_KEY = Symbol('i18next/PATH_KEY');
7458
+ function createProxy() {
7459
+ const state = [];
7460
+ const handler = Object.create(null);
7461
+ let proxy;
7462
+ handler.get = (target, key) => {
7463
+ proxy?.revoke?.();
7464
+ if (key === PATH_KEY) return state;
7465
+ state.push(key);
7466
+ proxy = Proxy.revocable(target, handler);
7467
+ return proxy.proxy;
7468
+ };
7469
+ return Proxy.revocable(Object.create(null), handler).proxy;
7470
+ }
7471
+ function keysFromSelector(selector, opts) {
7472
+ const {
7473
+ [PATH_KEY]: path
7474
+ } = selector(createProxy());
7475
+ return path.join(opts?.keySeparator ?? '.');
7476
+ }
7477
+
7478
+ const checkedLoadedFor = {};
7479
+ const shouldHandleAsObject = res => !isString(res) && typeof res !== 'boolean' && typeof res !== 'number';
7480
+ class Translator extends EventEmitter {
7481
+ constructor(services, options = {}) {
7482
+ super();
7483
+ copy(['resourceStore', 'languageUtils', 'pluralResolver', 'interpolator', 'backendConnector', 'i18nFormat', 'utils'], services, this);
7484
+ this.options = options;
7485
+ if (this.options.keySeparator === undefined) {
7486
+ this.options.keySeparator = '.';
7487
+ }
7488
+ this.logger = baseLogger.create('translator');
7489
+ }
7490
+ changeLanguage(lng) {
7491
+ if (lng) this.language = lng;
7492
+ }
7493
+ exists(key, o = {
7494
+ interpolation: {}
7495
+ }) {
7496
+ const opt = {
7497
+ ...o
7498
+ };
7499
+ if (key == null) return false;
7500
+ const resolved = this.resolve(key, opt);
7501
+ if (resolved?.res === undefined) return false;
7502
+ const isObject = shouldHandleAsObject(resolved.res);
7503
+ if (opt.returnObjects === false && isObject) {
7504
+ return false;
7505
+ }
7506
+ return true;
7507
+ }
7508
+ extractFromKey(key, opt) {
7509
+ let nsSeparator = opt.nsSeparator !== undefined ? opt.nsSeparator : this.options.nsSeparator;
7510
+ if (nsSeparator === undefined) nsSeparator = ':';
7511
+ const keySeparator = opt.keySeparator !== undefined ? opt.keySeparator : this.options.keySeparator;
7512
+ let namespaces = opt.ns || this.options.defaultNS || [];
7513
+ const wouldCheckForNsInKey = nsSeparator && key.indexOf(nsSeparator) > -1;
7514
+ const seemsNaturalLanguage = !this.options.userDefinedKeySeparator && !opt.keySeparator && !this.options.userDefinedNsSeparator && !opt.nsSeparator && !looksLikeObjectPath(key, nsSeparator, keySeparator);
7515
+ if (wouldCheckForNsInKey && !seemsNaturalLanguage) {
7516
+ const m = key.match(this.interpolator.nestingRegexp);
7517
+ if (m && m.length > 0) {
7518
+ return {
7519
+ key,
7520
+ namespaces: isString(namespaces) ? [namespaces] : namespaces
7521
+ };
7522
+ }
7523
+ const parts = key.split(nsSeparator);
7524
+ if (nsSeparator !== keySeparator || nsSeparator === keySeparator && this.options.ns.indexOf(parts[0]) > -1) namespaces = parts.shift();
7525
+ key = parts.join(keySeparator);
7526
+ }
7527
+ return {
7528
+ key,
7529
+ namespaces: isString(namespaces) ? [namespaces] : namespaces
7530
+ };
7531
+ }
7532
+ translate(keys, o, lastKey) {
7533
+ let opt = typeof o === 'object' ? {
7534
+ ...o
7535
+ } : o;
7536
+ if (typeof opt !== 'object' && this.options.overloadTranslationOptionHandler) {
7537
+ opt = this.options.overloadTranslationOptionHandler(arguments);
7538
+ }
7539
+ if (typeof opt === 'object') opt = {
7540
+ ...opt
7541
+ };
7542
+ if (!opt) opt = {};
7543
+ if (keys == null) return '';
7544
+ if (typeof keys === 'function') keys = keysFromSelector(keys, {
7545
+ ...this.options,
7546
+ ...opt
7547
+ });
7548
+ if (!Array.isArray(keys)) keys = [String(keys)];
7549
+ const returnDetails = opt.returnDetails !== undefined ? opt.returnDetails : this.options.returnDetails;
7550
+ const keySeparator = opt.keySeparator !== undefined ? opt.keySeparator : this.options.keySeparator;
7551
+ const {
7552
+ key,
7553
+ namespaces
7554
+ } = this.extractFromKey(keys[keys.length - 1], opt);
7555
+ const namespace = namespaces[namespaces.length - 1];
7556
+ let nsSeparator = opt.nsSeparator !== undefined ? opt.nsSeparator : this.options.nsSeparator;
7557
+ if (nsSeparator === undefined) nsSeparator = ':';
7558
+ const lng = opt.lng || this.language;
7559
+ const appendNamespaceToCIMode = opt.appendNamespaceToCIMode || this.options.appendNamespaceToCIMode;
7560
+ if (lng?.toLowerCase() === 'cimode') {
7561
+ if (appendNamespaceToCIMode) {
7562
+ if (returnDetails) {
7563
+ return {
7564
+ res: `${namespace}${nsSeparator}${key}`,
7565
+ usedKey: key,
7566
+ exactUsedKey: key,
7567
+ usedLng: lng,
7568
+ usedNS: namespace,
7569
+ usedParams: this.getUsedParamsDetails(opt)
7570
+ };
7571
+ }
7572
+ return `${namespace}${nsSeparator}${key}`;
7573
+ }
7574
+ if (returnDetails) {
7575
+ return {
7576
+ res: key,
7577
+ usedKey: key,
7578
+ exactUsedKey: key,
7579
+ usedLng: lng,
7580
+ usedNS: namespace,
7581
+ usedParams: this.getUsedParamsDetails(opt)
7582
+ };
7583
+ }
7584
+ return key;
7585
+ }
7586
+ const resolved = this.resolve(keys, opt);
7587
+ let res = resolved?.res;
7588
+ const resUsedKey = resolved?.usedKey || key;
7589
+ const resExactUsedKey = resolved?.exactUsedKey || key;
7590
+ const noObject = ['[object Number]', '[object Function]', '[object RegExp]'];
7591
+ const joinArrays = opt.joinArrays !== undefined ? opt.joinArrays : this.options.joinArrays;
7592
+ const handleAsObjectInI18nFormat = !this.i18nFormat || this.i18nFormat.handleAsObject;
7593
+ const needsPluralHandling = opt.count !== undefined && !isString(opt.count);
7594
+ const hasDefaultValue = Translator.hasDefaultValue(opt);
7595
+ const defaultValueSuffix = needsPluralHandling ? this.pluralResolver.getSuffix(lng, opt.count, opt) : '';
7596
+ const defaultValueSuffixOrdinalFallback = opt.ordinal && needsPluralHandling ? this.pluralResolver.getSuffix(lng, opt.count, {
7597
+ ordinal: false
7598
+ }) : '';
7599
+ const needsZeroSuffixLookup = needsPluralHandling && !opt.ordinal && opt.count === 0;
7600
+ const defaultValue = needsZeroSuffixLookup && opt[`defaultValue${this.options.pluralSeparator}zero`] || opt[`defaultValue${defaultValueSuffix}`] || opt[`defaultValue${defaultValueSuffixOrdinalFallback}`] || opt.defaultValue;
7601
+ let resForObjHndl = res;
7602
+ if (handleAsObjectInI18nFormat && !res && hasDefaultValue) {
7603
+ resForObjHndl = defaultValue;
7604
+ }
7605
+ const handleAsObject = shouldHandleAsObject(resForObjHndl);
7606
+ const resType = Object.prototype.toString.apply(resForObjHndl);
7607
+ if (handleAsObjectInI18nFormat && resForObjHndl && handleAsObject && noObject.indexOf(resType) < 0 && !(isString(joinArrays) && Array.isArray(resForObjHndl))) {
7608
+ if (!opt.returnObjects && !this.options.returnObjects) {
7609
+ if (!this.options.returnedObjectHandler) {
7610
+ this.logger.warn('accessing an object - but returnObjects options is not enabled!');
7611
+ }
7612
+ const r = this.options.returnedObjectHandler ? this.options.returnedObjectHandler(resUsedKey, resForObjHndl, {
7613
+ ...opt,
7614
+ ns: namespaces
7615
+ }) : `key '${key} (${this.language})' returned an object instead of string.`;
7616
+ if (returnDetails) {
7617
+ resolved.res = r;
7618
+ resolved.usedParams = this.getUsedParamsDetails(opt);
7619
+ return resolved;
7620
+ }
7621
+ return r;
7622
+ }
7623
+ if (keySeparator) {
7624
+ const resTypeIsArray = Array.isArray(resForObjHndl);
7625
+ const copy = resTypeIsArray ? [] : {};
7626
+ const newKeyToUse = resTypeIsArray ? resExactUsedKey : resUsedKey;
7627
+ for (const m in resForObjHndl) {
7628
+ if (Object.prototype.hasOwnProperty.call(resForObjHndl, m)) {
7629
+ const deepKey = `${newKeyToUse}${keySeparator}${m}`;
7630
+ if (hasDefaultValue && !res) {
7631
+ copy[m] = this.translate(deepKey, {
7632
+ ...opt,
7633
+ defaultValue: shouldHandleAsObject(defaultValue) ? defaultValue[m] : undefined,
7634
+ ...{
7635
+ joinArrays: false,
7636
+ ns: namespaces
7637
+ }
7638
+ });
7639
+ } else {
7640
+ copy[m] = this.translate(deepKey, {
7641
+ ...opt,
7642
+ ...{
7643
+ joinArrays: false,
7644
+ ns: namespaces
7645
+ }
7646
+ });
7647
+ }
7648
+ if (copy[m] === deepKey) copy[m] = resForObjHndl[m];
7649
+ }
7650
+ }
7651
+ res = copy;
7652
+ }
7653
+ } else if (handleAsObjectInI18nFormat && isString(joinArrays) && Array.isArray(res)) {
7654
+ res = res.join(joinArrays);
7655
+ if (res) res = this.extendTranslation(res, keys, opt, lastKey);
7656
+ } else {
7657
+ let usedDefault = false;
7658
+ let usedKey = false;
7659
+ if (!this.isValidLookup(res) && hasDefaultValue) {
7660
+ usedDefault = true;
7661
+ res = defaultValue;
7662
+ }
7663
+ if (!this.isValidLookup(res)) {
7664
+ usedKey = true;
7665
+ res = key;
7666
+ }
7667
+ const missingKeyNoValueFallbackToKey = opt.missingKeyNoValueFallbackToKey || this.options.missingKeyNoValueFallbackToKey;
7668
+ const resForMissing = missingKeyNoValueFallbackToKey && usedKey ? undefined : res;
7669
+ const updateMissing = hasDefaultValue && defaultValue !== res && this.options.updateMissing;
7670
+ if (usedKey || usedDefault || updateMissing) {
7671
+ this.logger.log(updateMissing ? 'updateKey' : 'missingKey', lng, namespace, key, updateMissing ? defaultValue : res);
7672
+ if (keySeparator) {
7673
+ const fk = this.resolve(key, {
7674
+ ...opt,
7675
+ keySeparator: false
7676
+ });
7677
+ if (fk && fk.res) this.logger.warn('Seems the loaded translations were in flat JSON format instead of nested. Either set keySeparator: false on init or make sure your translations are published in nested format.');
7678
+ }
7679
+ let lngs = [];
7680
+ const fallbackLngs = this.languageUtils.getFallbackCodes(this.options.fallbackLng, opt.lng || this.language);
7681
+ if (this.options.saveMissingTo === 'fallback' && fallbackLngs && fallbackLngs[0]) {
7682
+ for (let i = 0; i < fallbackLngs.length; i++) {
7683
+ lngs.push(fallbackLngs[i]);
7684
+ }
7685
+ } else if (this.options.saveMissingTo === 'all') {
7686
+ lngs = this.languageUtils.toResolveHierarchy(opt.lng || this.language);
7687
+ } else {
7688
+ lngs.push(opt.lng || this.language);
7689
+ }
7690
+ const send = (l, k, specificDefaultValue) => {
7691
+ const defaultForMissing = hasDefaultValue && specificDefaultValue !== res ? specificDefaultValue : resForMissing;
7692
+ if (this.options.missingKeyHandler) {
7693
+ this.options.missingKeyHandler(l, namespace, k, defaultForMissing, updateMissing, opt);
7694
+ } else if (this.backendConnector?.saveMissing) {
7695
+ this.backendConnector.saveMissing(l, namespace, k, defaultForMissing, updateMissing, opt);
7696
+ }
7697
+ this.emit('missingKey', l, namespace, k, res);
7698
+ };
7699
+ if (this.options.saveMissing) {
7700
+ if (this.options.saveMissingPlurals && needsPluralHandling) {
7701
+ lngs.forEach(language => {
7702
+ const suffixes = this.pluralResolver.getSuffixes(language, opt);
7703
+ if (needsZeroSuffixLookup && opt[`defaultValue${this.options.pluralSeparator}zero`] && suffixes.indexOf(`${this.options.pluralSeparator}zero`) < 0) {
7704
+ suffixes.push(`${this.options.pluralSeparator}zero`);
7705
+ }
7706
+ suffixes.forEach(suffix => {
7707
+ send([language], key + suffix, opt[`defaultValue${suffix}`] || defaultValue);
7708
+ });
7709
+ });
7710
+ } else {
7711
+ send(lngs, key, defaultValue);
7712
+ }
7713
+ }
7714
+ }
7715
+ res = this.extendTranslation(res, keys, opt, resolved, lastKey);
7716
+ if (usedKey && res === key && this.options.appendNamespaceToMissingKey) {
7717
+ res = `${namespace}${nsSeparator}${key}`;
7718
+ }
7719
+ if ((usedKey || usedDefault) && this.options.parseMissingKeyHandler) {
7720
+ res = this.options.parseMissingKeyHandler(this.options.appendNamespaceToMissingKey ? `${namespace}${nsSeparator}${key}` : key, usedDefault ? res : undefined, opt);
7721
+ }
7722
+ }
7723
+ if (returnDetails) {
7724
+ resolved.res = res;
7725
+ resolved.usedParams = this.getUsedParamsDetails(opt);
7726
+ return resolved;
7727
+ }
7728
+ return res;
7729
+ }
7730
+ extendTranslation(res, key, opt, resolved, lastKey) {
7731
+ if (this.i18nFormat?.parse) {
7732
+ res = this.i18nFormat.parse(res, {
7733
+ ...this.options.interpolation.defaultVariables,
7734
+ ...opt
7735
+ }, opt.lng || this.language || resolved.usedLng, resolved.usedNS, resolved.usedKey, {
7736
+ resolved
7737
+ });
7738
+ } else if (!opt.skipInterpolation) {
7739
+ if (opt.interpolation) this.interpolator.init({
7740
+ ...opt,
7741
+ ...{
7742
+ interpolation: {
7743
+ ...this.options.interpolation,
7744
+ ...opt.interpolation
7745
+ }
7746
+ }
7747
+ });
7748
+ const skipOnVariables = isString(res) && (opt?.interpolation?.skipOnVariables !== undefined ? opt.interpolation.skipOnVariables : this.options.interpolation.skipOnVariables);
7749
+ let nestBef;
7750
+ if (skipOnVariables) {
7751
+ const nb = res.match(this.interpolator.nestingRegexp);
7752
+ nestBef = nb && nb.length;
7753
+ }
7754
+ let data = opt.replace && !isString(opt.replace) ? opt.replace : opt;
7755
+ if (this.options.interpolation.defaultVariables) data = {
7756
+ ...this.options.interpolation.defaultVariables,
7757
+ ...data
7758
+ };
7759
+ res = this.interpolator.interpolate(res, data, opt.lng || this.language || resolved.usedLng, opt);
7760
+ if (skipOnVariables) {
7761
+ const na = res.match(this.interpolator.nestingRegexp);
7762
+ const nestAft = na && na.length;
7763
+ if (nestBef < nestAft) opt.nest = false;
7764
+ }
7765
+ if (!opt.lng && resolved && resolved.res) opt.lng = this.language || resolved.usedLng;
7766
+ if (opt.nest !== false) res = this.interpolator.nest(res, (...args) => {
7767
+ if (lastKey?.[0] === args[0] && !opt.context) {
7768
+ this.logger.warn(`It seems you are nesting recursively key: ${args[0]} in key: ${key[0]}`);
7769
+ return null;
7770
+ }
7771
+ return this.translate(...args, key);
7772
+ }, opt);
7773
+ if (opt.interpolation) this.interpolator.reset();
7774
+ }
7775
+ const postProcess = opt.postProcess || this.options.postProcess;
7776
+ const postProcessorNames = isString(postProcess) ? [postProcess] : postProcess;
7777
+ if (res != null && postProcessorNames?.length && opt.applyPostProcessor !== false) {
7778
+ res = postProcessor.handle(postProcessorNames, res, key, this.options && this.options.postProcessPassResolved ? {
7779
+ i18nResolved: {
7780
+ ...resolved,
7781
+ usedParams: this.getUsedParamsDetails(opt)
7782
+ },
7783
+ ...opt
7784
+ } : opt, this);
7785
+ }
7786
+ return res;
7787
+ }
7788
+ resolve(keys, opt = {}) {
7789
+ let found;
7790
+ let usedKey;
7791
+ let exactUsedKey;
7792
+ let usedLng;
7793
+ let usedNS;
7794
+ if (isString(keys)) keys = [keys];
7795
+ keys.forEach(k => {
7796
+ if (this.isValidLookup(found)) return;
7797
+ const extracted = this.extractFromKey(k, opt);
7798
+ const key = extracted.key;
7799
+ usedKey = key;
7800
+ let namespaces = extracted.namespaces;
7801
+ if (this.options.fallbackNS) namespaces = namespaces.concat(this.options.fallbackNS);
7802
+ const needsPluralHandling = opt.count !== undefined && !isString(opt.count);
7803
+ const needsZeroSuffixLookup = needsPluralHandling && !opt.ordinal && opt.count === 0;
7804
+ const needsContextHandling = opt.context !== undefined && (isString(opt.context) || typeof opt.context === 'number') && opt.context !== '';
7805
+ const codes = opt.lngs ? opt.lngs : this.languageUtils.toResolveHierarchy(opt.lng || this.language, opt.fallbackLng);
7806
+ namespaces.forEach(ns => {
7807
+ if (this.isValidLookup(found)) return;
7808
+ usedNS = ns;
7809
+ if (!checkedLoadedFor[`${codes[0]}-${ns}`] && this.utils?.hasLoadedNamespace && !this.utils?.hasLoadedNamespace(usedNS)) {
7810
+ checkedLoadedFor[`${codes[0]}-${ns}`] = true;
7811
+ this.logger.warn(`key "${usedKey}" for languages "${codes.join(', ')}" won't get resolved as namespace "${usedNS}" was not yet loaded`, 'This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!');
7812
+ }
7813
+ codes.forEach(code => {
7814
+ if (this.isValidLookup(found)) return;
7815
+ usedLng = code;
7816
+ const finalKeys = [key];
7817
+ if (this.i18nFormat?.addLookupKeys) {
7818
+ this.i18nFormat.addLookupKeys(finalKeys, key, code, ns, opt);
7819
+ } else {
7820
+ let pluralSuffix;
7821
+ if (needsPluralHandling) pluralSuffix = this.pluralResolver.getSuffix(code, opt.count, opt);
7822
+ const zeroSuffix = `${this.options.pluralSeparator}zero`;
7823
+ const ordinalPrefix = `${this.options.pluralSeparator}ordinal${this.options.pluralSeparator}`;
7824
+ if (needsPluralHandling) {
7825
+ if (opt.ordinal && pluralSuffix.indexOf(ordinalPrefix) === 0) {
7826
+ finalKeys.push(key + pluralSuffix.replace(ordinalPrefix, this.options.pluralSeparator));
7827
+ }
7828
+ finalKeys.push(key + pluralSuffix);
7829
+ if (needsZeroSuffixLookup) {
7830
+ finalKeys.push(key + zeroSuffix);
7831
+ }
7832
+ }
7833
+ if (needsContextHandling) {
7834
+ const contextKey = `${key}${this.options.contextSeparator || '_'}${opt.context}`;
7835
+ finalKeys.push(contextKey);
7836
+ if (needsPluralHandling) {
7837
+ if (opt.ordinal && pluralSuffix.indexOf(ordinalPrefix) === 0) {
7838
+ finalKeys.push(contextKey + pluralSuffix.replace(ordinalPrefix, this.options.pluralSeparator));
7839
+ }
7840
+ finalKeys.push(contextKey + pluralSuffix);
7841
+ if (needsZeroSuffixLookup) {
7842
+ finalKeys.push(contextKey + zeroSuffix);
7843
+ }
7844
+ }
7845
+ }
7846
+ }
7847
+ let possibleKey;
7848
+ while (possibleKey = finalKeys.pop()) {
7849
+ if (!this.isValidLookup(found)) {
7850
+ exactUsedKey = possibleKey;
7851
+ found = this.getResource(code, ns, possibleKey, opt);
7852
+ }
7853
+ }
7854
+ });
7855
+ });
7856
+ });
7857
+ return {
7858
+ res: found,
7859
+ usedKey,
7860
+ exactUsedKey,
7861
+ usedLng,
7862
+ usedNS
7863
+ };
7864
+ }
7865
+ isValidLookup(res) {
7866
+ return res !== undefined && !(!this.options.returnNull && res === null) && !(!this.options.returnEmptyString && res === '');
7867
+ }
7868
+ getResource(code, ns, key, options = {}) {
7869
+ if (this.i18nFormat?.getResource) return this.i18nFormat.getResource(code, ns, key, options);
7870
+ return this.resourceStore.getResource(code, ns, key, options);
7871
+ }
7872
+ getUsedParamsDetails(options = {}) {
7873
+ const optionsKeys = ['defaultValue', 'ordinal', 'context', 'replace', 'lng', 'lngs', 'fallbackLng', 'ns', 'keySeparator', 'nsSeparator', 'returnObjects', 'returnDetails', 'joinArrays', 'postProcess', 'interpolation'];
7874
+ const useOptionsReplaceForData = options.replace && !isString(options.replace);
7875
+ let data = useOptionsReplaceForData ? options.replace : options;
7876
+ if (useOptionsReplaceForData && typeof options.count !== 'undefined') {
7877
+ data.count = options.count;
7878
+ }
7879
+ if (this.options.interpolation.defaultVariables) {
7880
+ data = {
7881
+ ...this.options.interpolation.defaultVariables,
7882
+ ...data
7883
+ };
7884
+ }
7885
+ if (!useOptionsReplaceForData) {
7886
+ data = {
7887
+ ...data
7888
+ };
7889
+ for (const key of optionsKeys) {
7890
+ delete data[key];
7891
+ }
7892
+ }
7893
+ return data;
7894
+ }
7895
+ static hasDefaultValue(options) {
7896
+ const prefix = 'defaultValue';
7897
+ for (const option in options) {
7898
+ if (Object.prototype.hasOwnProperty.call(options, option) && prefix === option.substring(0, prefix.length) && undefined !== options[option]) {
7899
+ return true;
7900
+ }
7901
+ }
7902
+ return false;
7903
+ }
7904
+ }
7905
+
7906
+ class LanguageUtil {
7907
+ constructor(options) {
7908
+ this.options = options;
7909
+ this.supportedLngs = this.options.supportedLngs || false;
7910
+ this.logger = baseLogger.create('languageUtils');
7911
+ }
7912
+ getScriptPartFromCode(code) {
7913
+ code = getCleanedCode(code);
7914
+ if (!code || code.indexOf('-') < 0) return null;
7915
+ const p = code.split('-');
7916
+ if (p.length === 2) return null;
7917
+ p.pop();
7918
+ if (p[p.length - 1].toLowerCase() === 'x') return null;
7919
+ return this.formatLanguageCode(p.join('-'));
7920
+ }
7921
+ getLanguagePartFromCode(code) {
7922
+ code = getCleanedCode(code);
7923
+ if (!code || code.indexOf('-') < 0) return code;
7924
+ const p = code.split('-');
7925
+ return this.formatLanguageCode(p[0]);
7926
+ }
7927
+ formatLanguageCode(code) {
7928
+ if (isString(code) && code.indexOf('-') > -1) {
7929
+ let formattedCode;
7930
+ try {
7931
+ formattedCode = Intl.getCanonicalLocales(code)[0];
7932
+ } catch (e) {}
7933
+ if (formattedCode && this.options.lowerCaseLng) {
7934
+ formattedCode = formattedCode.toLowerCase();
7935
+ }
7936
+ if (formattedCode) return formattedCode;
7937
+ if (this.options.lowerCaseLng) {
7938
+ return code.toLowerCase();
7939
+ }
7940
+ return code;
7941
+ }
7942
+ return this.options.cleanCode || this.options.lowerCaseLng ? code.toLowerCase() : code;
7943
+ }
7944
+ isSupportedCode(code) {
7945
+ if (this.options.load === 'languageOnly' || this.options.nonExplicitSupportedLngs) {
7946
+ code = this.getLanguagePartFromCode(code);
7947
+ }
7948
+ return !this.supportedLngs || !this.supportedLngs.length || this.supportedLngs.indexOf(code) > -1;
7949
+ }
7950
+ getBestMatchFromCodes(codes) {
7951
+ if (!codes) return null;
7952
+ let found;
7953
+ codes.forEach(code => {
7954
+ if (found) return;
7955
+ const cleanedLng = this.formatLanguageCode(code);
7956
+ if (!this.options.supportedLngs || this.isSupportedCode(cleanedLng)) found = cleanedLng;
7957
+ });
7958
+ if (!found && this.options.supportedLngs) {
7959
+ codes.forEach(code => {
7960
+ if (found) return;
7961
+ const lngScOnly = this.getScriptPartFromCode(code);
7962
+ if (this.isSupportedCode(lngScOnly)) return found = lngScOnly;
7963
+ const lngOnly = this.getLanguagePartFromCode(code);
7964
+ if (this.isSupportedCode(lngOnly)) return found = lngOnly;
7965
+ found = this.options.supportedLngs.find(supportedLng => {
7966
+ if (supportedLng === lngOnly) return supportedLng;
7967
+ if (supportedLng.indexOf('-') < 0 && lngOnly.indexOf('-') < 0) return;
7968
+ if (supportedLng.indexOf('-') > 0 && lngOnly.indexOf('-') < 0 && supportedLng.substring(0, supportedLng.indexOf('-')) === lngOnly) return supportedLng;
7969
+ if (supportedLng.indexOf(lngOnly) === 0 && lngOnly.length > 1) return supportedLng;
7970
+ });
7971
+ });
7972
+ }
7973
+ if (!found) found = this.getFallbackCodes(this.options.fallbackLng)[0];
7974
+ return found;
7975
+ }
7976
+ getFallbackCodes(fallbacks, code) {
7977
+ if (!fallbacks) return [];
7978
+ if (typeof fallbacks === 'function') fallbacks = fallbacks(code);
7979
+ if (isString(fallbacks)) fallbacks = [fallbacks];
7980
+ if (Array.isArray(fallbacks)) return fallbacks;
7981
+ if (!code) return fallbacks.default || [];
7982
+ let found = fallbacks[code];
7983
+ if (!found) found = fallbacks[this.getScriptPartFromCode(code)];
7984
+ if (!found) found = fallbacks[this.formatLanguageCode(code)];
7985
+ if (!found) found = fallbacks[this.getLanguagePartFromCode(code)];
7986
+ if (!found) found = fallbacks.default;
7987
+ return found || [];
7988
+ }
7989
+ toResolveHierarchy(code, fallbackCode) {
7990
+ const fallbackCodes = this.getFallbackCodes((fallbackCode === false ? [] : fallbackCode) || this.options.fallbackLng || [], code);
7991
+ const codes = [];
7992
+ const addCode = c => {
7993
+ if (!c) return;
7994
+ if (this.isSupportedCode(c)) {
7995
+ codes.push(c);
7996
+ } else {
7997
+ this.logger.warn(`rejecting language code not found in supportedLngs: ${c}`);
7998
+ }
7999
+ };
8000
+ if (isString(code) && (code.indexOf('-') > -1 || code.indexOf('_') > -1)) {
8001
+ if (this.options.load !== 'languageOnly') addCode(this.formatLanguageCode(code));
8002
+ if (this.options.load !== 'languageOnly' && this.options.load !== 'currentOnly') addCode(this.getScriptPartFromCode(code));
8003
+ if (this.options.load !== 'currentOnly') addCode(this.getLanguagePartFromCode(code));
8004
+ } else if (isString(code)) {
8005
+ addCode(this.formatLanguageCode(code));
8006
+ }
8007
+ fallbackCodes.forEach(fc => {
8008
+ if (codes.indexOf(fc) < 0) addCode(this.formatLanguageCode(fc));
8009
+ });
8010
+ return codes;
8011
+ }
8012
+ }
8013
+
8014
+ const suffixesOrder = {
8015
+ zero: 0,
8016
+ one: 1,
8017
+ two: 2,
8018
+ few: 3,
8019
+ many: 4,
8020
+ other: 5
8021
+ };
8022
+ const dummyRule = {
8023
+ select: count => count === 1 ? 'one' : 'other',
8024
+ resolvedOptions: () => ({
8025
+ pluralCategories: ['one', 'other']
8026
+ })
8027
+ };
8028
+ class PluralResolver {
8029
+ constructor(languageUtils, options = {}) {
8030
+ this.languageUtils = languageUtils;
8031
+ this.options = options;
8032
+ this.logger = baseLogger.create('pluralResolver');
8033
+ this.pluralRulesCache = {};
8034
+ }
8035
+ clearCache() {
8036
+ this.pluralRulesCache = {};
8037
+ }
8038
+ getRule(code, options = {}) {
8039
+ const cleanedCode = getCleanedCode(code === 'dev' ? 'en' : code);
8040
+ const type = options.ordinal ? 'ordinal' : 'cardinal';
8041
+ const cacheKey = JSON.stringify({
8042
+ cleanedCode,
8043
+ type
8044
+ });
8045
+ if (cacheKey in this.pluralRulesCache) {
8046
+ return this.pluralRulesCache[cacheKey];
8047
+ }
8048
+ let rule;
8049
+ try {
8050
+ rule = new Intl.PluralRules(cleanedCode, {
8051
+ type
8052
+ });
8053
+ } catch (err) {
8054
+ if (!Intl) {
8055
+ this.logger.error('No Intl support, please use an Intl polyfill!');
8056
+ return dummyRule;
8057
+ }
8058
+ if (!code.match(/-|_/)) return dummyRule;
8059
+ const lngPart = this.languageUtils.getLanguagePartFromCode(code);
8060
+ rule = this.getRule(lngPart, options);
8061
+ }
8062
+ this.pluralRulesCache[cacheKey] = rule;
8063
+ return rule;
8064
+ }
8065
+ needsPlural(code, options = {}) {
8066
+ let rule = this.getRule(code, options);
8067
+ if (!rule) rule = this.getRule('dev', options);
8068
+ return rule?.resolvedOptions().pluralCategories.length > 1;
8069
+ }
8070
+ getPluralFormsOfKey(code, key, options = {}) {
8071
+ return this.getSuffixes(code, options).map(suffix => `${key}${suffix}`);
8072
+ }
8073
+ getSuffixes(code, options = {}) {
8074
+ let rule = this.getRule(code, options);
8075
+ if (!rule) rule = this.getRule('dev', options);
8076
+ if (!rule) return [];
8077
+ return rule.resolvedOptions().pluralCategories.sort((pluralCategory1, pluralCategory2) => suffixesOrder[pluralCategory1] - suffixesOrder[pluralCategory2]).map(pluralCategory => `${this.options.prepend}${options.ordinal ? `ordinal${this.options.prepend}` : ''}${pluralCategory}`);
8078
+ }
8079
+ getSuffix(code, count, options = {}) {
8080
+ const rule = this.getRule(code, options);
8081
+ if (rule) {
8082
+ return `${this.options.prepend}${options.ordinal ? `ordinal${this.options.prepend}` : ''}${rule.select(count)}`;
8083
+ }
8084
+ this.logger.warn(`no plural rule found for: ${code}`);
8085
+ return this.getSuffix('dev', count, options);
8086
+ }
8087
+ }
8088
+
8089
+ const deepFindWithDefaults = (data, defaultData, key, keySeparator = '.', ignoreJSONStructure = true) => {
8090
+ let path = getPathWithDefaults(data, defaultData, key);
8091
+ if (!path && ignoreJSONStructure && isString(key)) {
8092
+ path = deepFind(data, key, keySeparator);
8093
+ if (path === undefined) path = deepFind(defaultData, key, keySeparator);
8094
+ }
8095
+ return path;
8096
+ };
8097
+ const regexSafe = val => val.replace(/\$/g, '$$$$');
8098
+ class Interpolator {
8099
+ constructor(options = {}) {
8100
+ this.logger = baseLogger.create('interpolator');
8101
+ this.options = options;
8102
+ this.format = options?.interpolation?.format || (value => value);
8103
+ this.init(options);
8104
+ }
8105
+ init(options = {}) {
8106
+ if (!options.interpolation) options.interpolation = {
8107
+ escapeValue: true
8108
+ };
8109
+ const {
8110
+ escape: escape$1,
8111
+ escapeValue,
8112
+ useRawValueToEscape,
8113
+ prefix,
8114
+ prefixEscaped,
8115
+ suffix,
8116
+ suffixEscaped,
8117
+ formatSeparator,
8118
+ unescapeSuffix,
8119
+ unescapePrefix,
8120
+ nestingPrefix,
8121
+ nestingPrefixEscaped,
8122
+ nestingSuffix,
8123
+ nestingSuffixEscaped,
8124
+ nestingOptionsSeparator,
8125
+ maxReplaces,
8126
+ alwaysFormat
8127
+ } = options.interpolation;
8128
+ this.escape = escape$1 !== undefined ? escape$1 : escape;
8129
+ this.escapeValue = escapeValue !== undefined ? escapeValue : true;
8130
+ this.useRawValueToEscape = useRawValueToEscape !== undefined ? useRawValueToEscape : false;
8131
+ this.prefix = prefix ? regexEscape(prefix) : prefixEscaped || '{{';
8132
+ this.suffix = suffix ? regexEscape(suffix) : suffixEscaped || '}}';
8133
+ this.formatSeparator = formatSeparator || ',';
8134
+ this.unescapePrefix = unescapeSuffix ? '' : unescapePrefix || '-';
8135
+ this.unescapeSuffix = this.unescapePrefix ? '' : unescapeSuffix || '';
8136
+ this.nestingPrefix = nestingPrefix ? regexEscape(nestingPrefix) : nestingPrefixEscaped || regexEscape('$t(');
8137
+ this.nestingSuffix = nestingSuffix ? regexEscape(nestingSuffix) : nestingSuffixEscaped || regexEscape(')');
8138
+ this.nestingOptionsSeparator = nestingOptionsSeparator || ',';
8139
+ this.maxReplaces = maxReplaces || 1000;
8140
+ this.alwaysFormat = alwaysFormat !== undefined ? alwaysFormat : false;
8141
+ this.resetRegExp();
8142
+ }
8143
+ reset() {
8144
+ if (this.options) this.init(this.options);
8145
+ }
8146
+ resetRegExp() {
8147
+ const getOrResetRegExp = (existingRegExp, pattern) => {
8148
+ if (existingRegExp?.source === pattern) {
8149
+ existingRegExp.lastIndex = 0;
8150
+ return existingRegExp;
8151
+ }
8152
+ return new RegExp(pattern, 'g');
8153
+ };
8154
+ this.regexp = getOrResetRegExp(this.regexp, `${this.prefix}(.+?)${this.suffix}`);
8155
+ this.regexpUnescape = getOrResetRegExp(this.regexpUnescape, `${this.prefix}${this.unescapePrefix}(.+?)${this.unescapeSuffix}${this.suffix}`);
8156
+ this.nestingRegexp = getOrResetRegExp(this.nestingRegexp, `${this.nestingPrefix}((?:[^()"']+|"[^"]*"|'[^']*'|\\((?:[^()]|"[^"]*"|'[^']*')*\\))*?)${this.nestingSuffix}`);
8157
+ }
8158
+ interpolate(str, data, lng, options) {
8159
+ let match;
8160
+ let value;
8161
+ let replaces;
8162
+ const defaultData = this.options && this.options.interpolation && this.options.interpolation.defaultVariables || {};
8163
+ const handleFormat = key => {
8164
+ if (key.indexOf(this.formatSeparator) < 0) {
8165
+ const path = deepFindWithDefaults(data, defaultData, key, this.options.keySeparator, this.options.ignoreJSONStructure);
8166
+ return this.alwaysFormat ? this.format(path, undefined, lng, {
8167
+ ...options,
8168
+ ...data,
8169
+ interpolationkey: key
8170
+ }) : path;
8171
+ }
8172
+ const p = key.split(this.formatSeparator);
8173
+ const k = p.shift().trim();
8174
+ const f = p.join(this.formatSeparator).trim();
8175
+ return this.format(deepFindWithDefaults(data, defaultData, k, this.options.keySeparator, this.options.ignoreJSONStructure), f, lng, {
8176
+ ...options,
8177
+ ...data,
8178
+ interpolationkey: k
8179
+ });
8180
+ };
8181
+ this.resetRegExp();
8182
+ const missingInterpolationHandler = options?.missingInterpolationHandler || this.options.missingInterpolationHandler;
8183
+ const skipOnVariables = options?.interpolation?.skipOnVariables !== undefined ? options.interpolation.skipOnVariables : this.options.interpolation.skipOnVariables;
8184
+ const todos = [{
8185
+ regex: this.regexpUnescape,
8186
+ safeValue: val => regexSafe(val)
8187
+ }, {
8188
+ regex: this.regexp,
8189
+ safeValue: val => this.escapeValue ? regexSafe(this.escape(val)) : regexSafe(val)
8190
+ }];
8191
+ todos.forEach(todo => {
8192
+ replaces = 0;
8193
+ while (match = todo.regex.exec(str)) {
8194
+ const matchedVar = match[1].trim();
8195
+ value = handleFormat(matchedVar);
8196
+ if (value === undefined) {
8197
+ if (typeof missingInterpolationHandler === 'function') {
8198
+ const temp = missingInterpolationHandler(str, match, options);
8199
+ value = isString(temp) ? temp : '';
8200
+ } else if (options && Object.prototype.hasOwnProperty.call(options, matchedVar)) {
8201
+ value = '';
8202
+ } else if (skipOnVariables) {
8203
+ value = match[0];
8204
+ continue;
8205
+ } else {
8206
+ this.logger.warn(`missed to pass in variable ${matchedVar} for interpolating ${str}`);
8207
+ value = '';
8208
+ }
8209
+ } else if (!isString(value) && !this.useRawValueToEscape) {
8210
+ value = makeString(value);
8211
+ }
8212
+ const safeValue = todo.safeValue(value);
8213
+ str = str.replace(match[0], safeValue);
8214
+ if (skipOnVariables) {
8215
+ todo.regex.lastIndex += value.length;
8216
+ todo.regex.lastIndex -= match[0].length;
8217
+ } else {
8218
+ todo.regex.lastIndex = 0;
8219
+ }
8220
+ replaces++;
8221
+ if (replaces >= this.maxReplaces) {
8222
+ break;
8223
+ }
8224
+ }
8225
+ });
8226
+ return str;
8227
+ }
8228
+ nest(str, fc, options = {}) {
8229
+ let match;
8230
+ let value;
8231
+ let clonedOptions;
8232
+ const handleHasOptions = (key, inheritedOptions) => {
8233
+ const sep = this.nestingOptionsSeparator;
8234
+ if (key.indexOf(sep) < 0) return key;
8235
+ const c = key.split(new RegExp(`${sep}[ ]*{`));
8236
+ let optionsString = `{${c[1]}`;
8237
+ key = c[0];
8238
+ optionsString = this.interpolate(optionsString, clonedOptions);
8239
+ const matchedSingleQuotes = optionsString.match(/'/g);
8240
+ const matchedDoubleQuotes = optionsString.match(/"/g);
8241
+ if ((matchedSingleQuotes?.length ?? 0) % 2 === 0 && !matchedDoubleQuotes || matchedDoubleQuotes.length % 2 !== 0) {
8242
+ optionsString = optionsString.replace(/'/g, '"');
8243
+ }
8244
+ try {
8245
+ clonedOptions = JSON.parse(optionsString);
8246
+ if (inheritedOptions) clonedOptions = {
8247
+ ...inheritedOptions,
8248
+ ...clonedOptions
8249
+ };
8250
+ } catch (e) {
8251
+ this.logger.warn(`failed parsing options string in nesting for key ${key}`, e);
8252
+ return `${key}${sep}${optionsString}`;
8253
+ }
8254
+ if (clonedOptions.defaultValue && clonedOptions.defaultValue.indexOf(this.prefix) > -1) delete clonedOptions.defaultValue;
8255
+ return key;
8256
+ };
8257
+ while (match = this.nestingRegexp.exec(str)) {
8258
+ let formatters = [];
8259
+ clonedOptions = {
8260
+ ...options
8261
+ };
8262
+ clonedOptions = clonedOptions.replace && !isString(clonedOptions.replace) ? clonedOptions.replace : clonedOptions;
8263
+ clonedOptions.applyPostProcessor = false;
8264
+ delete clonedOptions.defaultValue;
8265
+ const keyEndIndex = /{.*}/.test(match[1]) ? match[1].lastIndexOf('}') + 1 : match[1].indexOf(this.formatSeparator);
8266
+ if (keyEndIndex !== -1) {
8267
+ formatters = match[1].slice(keyEndIndex).split(this.formatSeparator).map(elem => elem.trim()).filter(Boolean);
8268
+ match[1] = match[1].slice(0, keyEndIndex);
8269
+ }
8270
+ value = fc(handleHasOptions.call(this, match[1].trim(), clonedOptions), clonedOptions);
8271
+ if (value && match[0] === str && !isString(value)) return value;
8272
+ if (!isString(value)) value = makeString(value);
8273
+ if (!value) {
8274
+ this.logger.warn(`missed to resolve ${match[1]} for nesting ${str}`);
8275
+ value = '';
8276
+ }
8277
+ if (formatters.length) {
8278
+ value = formatters.reduce((v, f) => this.format(v, f, options.lng, {
8279
+ ...options,
8280
+ interpolationkey: match[1].trim()
8281
+ }), value.trim());
8282
+ }
8283
+ str = str.replace(match[0], value);
8284
+ this.regexp.lastIndex = 0;
8285
+ }
8286
+ return str;
8287
+ }
8288
+ }
8289
+
8290
+ const parseFormatStr = formatStr => {
8291
+ let formatName = formatStr.toLowerCase().trim();
8292
+ const formatOptions = {};
8293
+ if (formatStr.indexOf('(') > -1) {
8294
+ const p = formatStr.split('(');
8295
+ formatName = p[0].toLowerCase().trim();
8296
+ const optStr = p[1].substring(0, p[1].length - 1);
8297
+ if (formatName === 'currency' && optStr.indexOf(':') < 0) {
8298
+ if (!formatOptions.currency) formatOptions.currency = optStr.trim();
8299
+ } else if (formatName === 'relativetime' && optStr.indexOf(':') < 0) {
8300
+ if (!formatOptions.range) formatOptions.range = optStr.trim();
8301
+ } else {
8302
+ const opts = optStr.split(';');
8303
+ opts.forEach(opt => {
8304
+ if (opt) {
8305
+ const [key, ...rest] = opt.split(':');
8306
+ const val = rest.join(':').trim().replace(/^'+|'+$/g, '');
8307
+ const trimmedKey = key.trim();
8308
+ if (!formatOptions[trimmedKey]) formatOptions[trimmedKey] = val;
8309
+ if (val === 'false') formatOptions[trimmedKey] = false;
8310
+ if (val === 'true') formatOptions[trimmedKey] = true;
8311
+ if (!isNaN(val)) formatOptions[trimmedKey] = parseInt(val, 10);
8312
+ }
8313
+ });
8314
+ }
8315
+ }
8316
+ return {
8317
+ formatName,
8318
+ formatOptions
8319
+ };
8320
+ };
8321
+ const createCachedFormatter = fn => {
8322
+ const cache = {};
8323
+ return (v, l, o) => {
8324
+ let optForCache = o;
8325
+ if (o && o.interpolationkey && o.formatParams && o.formatParams[o.interpolationkey] && o[o.interpolationkey]) {
8326
+ optForCache = {
8327
+ ...optForCache,
8328
+ [o.interpolationkey]: undefined
8329
+ };
8330
+ }
8331
+ const key = l + JSON.stringify(optForCache);
8332
+ let frm = cache[key];
8333
+ if (!frm) {
8334
+ frm = fn(getCleanedCode(l), o);
8335
+ cache[key] = frm;
8336
+ }
8337
+ return frm(v);
8338
+ };
8339
+ };
8340
+ const createNonCachedFormatter = fn => (v, l, o) => fn(getCleanedCode(l), o)(v);
8341
+ class Formatter {
8342
+ constructor(options = {}) {
8343
+ this.logger = baseLogger.create('formatter');
8344
+ this.options = options;
8345
+ this.init(options);
8346
+ }
8347
+ init(services, options = {
8348
+ interpolation: {}
8349
+ }) {
8350
+ this.formatSeparator = options.interpolation.formatSeparator || ',';
8351
+ const cf = options.cacheInBuiltFormats ? createCachedFormatter : createNonCachedFormatter;
8352
+ this.formats = {
8353
+ number: cf((lng, opt) => {
8354
+ const formatter = new Intl.NumberFormat(lng, {
8355
+ ...opt
8356
+ });
8357
+ return val => formatter.format(val);
8358
+ }),
8359
+ currency: cf((lng, opt) => {
8360
+ const formatter = new Intl.NumberFormat(lng, {
8361
+ ...opt,
8362
+ style: 'currency'
8363
+ });
8364
+ return val => formatter.format(val);
8365
+ }),
8366
+ datetime: cf((lng, opt) => {
8367
+ const formatter = new Intl.DateTimeFormat(lng, {
8368
+ ...opt
8369
+ });
8370
+ return val => formatter.format(val);
8371
+ }),
8372
+ relativetime: cf((lng, opt) => {
8373
+ const formatter = new Intl.RelativeTimeFormat(lng, {
8374
+ ...opt
8375
+ });
8376
+ return val => formatter.format(val, opt.range || 'day');
8377
+ }),
8378
+ list: cf((lng, opt) => {
8379
+ const formatter = new Intl.ListFormat(lng, {
8380
+ ...opt
8381
+ });
8382
+ return val => formatter.format(val);
8383
+ })
8384
+ };
8385
+ }
8386
+ add(name, fc) {
8387
+ this.formats[name.toLowerCase().trim()] = fc;
8388
+ }
8389
+ addCached(name, fc) {
8390
+ this.formats[name.toLowerCase().trim()] = createCachedFormatter(fc);
8391
+ }
8392
+ format(value, format, lng, options = {}) {
8393
+ const formats = format.split(this.formatSeparator);
8394
+ if (formats.length > 1 && formats[0].indexOf('(') > 1 && formats[0].indexOf(')') < 0 && formats.find(f => f.indexOf(')') > -1)) {
8395
+ const lastIndex = formats.findIndex(f => f.indexOf(')') > -1);
8396
+ formats[0] = [formats[0], ...formats.splice(1, lastIndex)].join(this.formatSeparator);
8397
+ }
8398
+ const result = formats.reduce((mem, f) => {
8399
+ const {
8400
+ formatName,
8401
+ formatOptions
8402
+ } = parseFormatStr(f);
8403
+ if (this.formats[formatName]) {
8404
+ let formatted = mem;
8405
+ try {
8406
+ const valOptions = options?.formatParams?.[options.interpolationkey] || {};
8407
+ const l = valOptions.locale || valOptions.lng || options.locale || options.lng || lng;
8408
+ formatted = this.formats[formatName](mem, l, {
8409
+ ...formatOptions,
8410
+ ...options,
8411
+ ...valOptions
8412
+ });
8413
+ } catch (error) {
8414
+ this.logger.warn(error);
8415
+ }
8416
+ return formatted;
8417
+ } else {
8418
+ this.logger.warn(`there was no format function for ${formatName}`);
8419
+ }
8420
+ return mem;
8421
+ }, value);
8422
+ return result;
8423
+ }
8424
+ }
8425
+
8426
+ const removePending = (q, name) => {
8427
+ if (q.pending[name] !== undefined) {
8428
+ delete q.pending[name];
8429
+ q.pendingCount--;
8430
+ }
8431
+ };
8432
+ class Connector extends EventEmitter {
8433
+ constructor(backend, store, services, options = {}) {
8434
+ super();
8435
+ this.backend = backend;
8436
+ this.store = store;
8437
+ this.services = services;
8438
+ this.languageUtils = services.languageUtils;
8439
+ this.options = options;
8440
+ this.logger = baseLogger.create('backendConnector');
8441
+ this.waitingReads = [];
8442
+ this.maxParallelReads = options.maxParallelReads || 10;
8443
+ this.readingCalls = 0;
8444
+ this.maxRetries = options.maxRetries >= 0 ? options.maxRetries : 5;
8445
+ this.retryTimeout = options.retryTimeout >= 1 ? options.retryTimeout : 350;
8446
+ this.state = {};
8447
+ this.queue = [];
8448
+ this.backend?.init?.(services, options.backend, options);
8449
+ }
8450
+ queueLoad(languages, namespaces, options, callback) {
8451
+ const toLoad = {};
8452
+ const pending = {};
8453
+ const toLoadLanguages = {};
8454
+ const toLoadNamespaces = {};
8455
+ languages.forEach(lng => {
8456
+ let hasAllNamespaces = true;
8457
+ namespaces.forEach(ns => {
8458
+ const name = `${lng}|${ns}`;
8459
+ if (!options.reload && this.store.hasResourceBundle(lng, ns)) {
8460
+ this.state[name] = 2;
8461
+ } else if (this.state[name] < 0) ; else if (this.state[name] === 1) {
8462
+ if (pending[name] === undefined) pending[name] = true;
8463
+ } else {
8464
+ this.state[name] = 1;
8465
+ hasAllNamespaces = false;
8466
+ if (pending[name] === undefined) pending[name] = true;
8467
+ if (toLoad[name] === undefined) toLoad[name] = true;
8468
+ if (toLoadNamespaces[ns] === undefined) toLoadNamespaces[ns] = true;
8469
+ }
8470
+ });
8471
+ if (!hasAllNamespaces) toLoadLanguages[lng] = true;
8472
+ });
8473
+ if (Object.keys(toLoad).length || Object.keys(pending).length) {
8474
+ this.queue.push({
8475
+ pending,
8476
+ pendingCount: Object.keys(pending).length,
8477
+ loaded: {},
8478
+ errors: [],
8479
+ callback
8480
+ });
8481
+ }
8482
+ return {
8483
+ toLoad: Object.keys(toLoad),
8484
+ pending: Object.keys(pending),
8485
+ toLoadLanguages: Object.keys(toLoadLanguages),
8486
+ toLoadNamespaces: Object.keys(toLoadNamespaces)
8487
+ };
8488
+ }
8489
+ loaded(name, err, data) {
8490
+ const s = name.split('|');
8491
+ const lng = s[0];
8492
+ const ns = s[1];
8493
+ if (err) this.emit('failedLoading', lng, ns, err);
8494
+ if (!err && data) {
8495
+ this.store.addResourceBundle(lng, ns, data, undefined, undefined, {
8496
+ skipCopy: true
8497
+ });
8498
+ }
8499
+ this.state[name] = err ? -1 : 2;
8500
+ if (err && data) this.state[name] = 0;
8501
+ const loaded = {};
8502
+ this.queue.forEach(q => {
8503
+ pushPath(q.loaded, [lng], ns);
8504
+ removePending(q, name);
8505
+ if (err) q.errors.push(err);
8506
+ if (q.pendingCount === 0 && !q.done) {
8507
+ Object.keys(q.loaded).forEach(l => {
8508
+ if (!loaded[l]) loaded[l] = {};
8509
+ const loadedKeys = q.loaded[l];
8510
+ if (loadedKeys.length) {
8511
+ loadedKeys.forEach(n => {
8512
+ if (loaded[l][n] === undefined) loaded[l][n] = true;
8513
+ });
8514
+ }
8515
+ });
8516
+ q.done = true;
8517
+ if (q.errors.length) {
8518
+ q.callback(q.errors);
8519
+ } else {
8520
+ q.callback();
8521
+ }
8522
+ }
8523
+ });
8524
+ this.emit('loaded', loaded);
8525
+ this.queue = this.queue.filter(q => !q.done);
8526
+ }
8527
+ read(lng, ns, fcName, tried = 0, wait = this.retryTimeout, callback) {
8528
+ if (!lng.length) return callback(null, {});
8529
+ if (this.readingCalls >= this.maxParallelReads) {
8530
+ this.waitingReads.push({
8531
+ lng,
8532
+ ns,
8533
+ fcName,
8534
+ tried,
8535
+ wait,
8536
+ callback
8537
+ });
8538
+ return;
8539
+ }
8540
+ this.readingCalls++;
8541
+ const resolver = (err, data) => {
8542
+ this.readingCalls--;
8543
+ if (this.waitingReads.length > 0) {
8544
+ const next = this.waitingReads.shift();
8545
+ this.read(next.lng, next.ns, next.fcName, next.tried, next.wait, next.callback);
8546
+ }
8547
+ if (err && data && tried < this.maxRetries) {
8548
+ setTimeout(() => {
8549
+ this.read.call(this, lng, ns, fcName, tried + 1, wait * 2, callback);
8550
+ }, wait);
8551
+ return;
8552
+ }
8553
+ callback(err, data);
8554
+ };
8555
+ const fc = this.backend[fcName].bind(this.backend);
8556
+ if (fc.length === 2) {
8557
+ try {
8558
+ const r = fc(lng, ns);
8559
+ if (r && typeof r.then === 'function') {
8560
+ r.then(data => resolver(null, data)).catch(resolver);
8561
+ } else {
8562
+ resolver(null, r);
8563
+ }
8564
+ } catch (err) {
8565
+ resolver(err);
8566
+ }
8567
+ return;
8568
+ }
8569
+ return fc(lng, ns, resolver);
8570
+ }
8571
+ prepareLoading(languages, namespaces, options = {}, callback) {
8572
+ if (!this.backend) {
8573
+ this.logger.warn('No backend was added via i18next.use. Will not load resources.');
8574
+ return callback && callback();
8575
+ }
8576
+ if (isString(languages)) languages = this.languageUtils.toResolveHierarchy(languages);
8577
+ if (isString(namespaces)) namespaces = [namespaces];
8578
+ const toLoad = this.queueLoad(languages, namespaces, options, callback);
8579
+ if (!toLoad.toLoad.length) {
8580
+ if (!toLoad.pending.length) callback();
8581
+ return null;
8582
+ }
8583
+ toLoad.toLoad.forEach(name => {
8584
+ this.loadOne(name);
8585
+ });
8586
+ }
8587
+ load(languages, namespaces, callback) {
8588
+ this.prepareLoading(languages, namespaces, {}, callback);
8589
+ }
8590
+ reload(languages, namespaces, callback) {
8591
+ this.prepareLoading(languages, namespaces, {
8592
+ reload: true
8593
+ }, callback);
8594
+ }
8595
+ loadOne(name, prefix = '') {
8596
+ const s = name.split('|');
8597
+ const lng = s[0];
8598
+ const ns = s[1];
8599
+ this.read(lng, ns, 'read', undefined, undefined, (err, data) => {
8600
+ if (err) this.logger.warn(`${prefix}loading namespace ${ns} for language ${lng} failed`, err);
8601
+ if (!err && data) this.logger.log(`${prefix}loaded namespace ${ns} for language ${lng}`, data);
8602
+ this.loaded(name, err, data);
8603
+ });
8604
+ }
8605
+ saveMissing(languages, namespace, key, fallbackValue, isUpdate, options = {}, clb = () => {}) {
8606
+ if (this.services?.utils?.hasLoadedNamespace && !this.services?.utils?.hasLoadedNamespace(namespace)) {
8607
+ this.logger.warn(`did not save key "${key}" as the namespace "${namespace}" was not yet loaded`, 'This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!');
8608
+ return;
8609
+ }
8610
+ if (key === undefined || key === null || key === '') return;
8611
+ if (this.backend?.create) {
8612
+ const opts = {
8613
+ ...options,
8614
+ isUpdate
8615
+ };
8616
+ const fc = this.backend.create.bind(this.backend);
8617
+ if (fc.length < 6) {
8618
+ try {
8619
+ let r;
8620
+ if (fc.length === 5) {
8621
+ r = fc(languages, namespace, key, fallbackValue, opts);
8622
+ } else {
8623
+ r = fc(languages, namespace, key, fallbackValue);
8624
+ }
8625
+ if (r && typeof r.then === 'function') {
8626
+ r.then(data => clb(null, data)).catch(clb);
8627
+ } else {
8628
+ clb(null, r);
8629
+ }
8630
+ } catch (err) {
8631
+ clb(err);
8632
+ }
8633
+ } else {
8634
+ fc(languages, namespace, key, fallbackValue, clb, opts);
8635
+ }
8636
+ }
8637
+ if (!languages || !languages[0]) return;
8638
+ this.store.addResource(languages[0], namespace, key, fallbackValue);
8639
+ }
8640
+ }
8641
+
8642
+ const get = () => ({
8643
+ debug: false,
8644
+ initAsync: true,
8645
+ ns: ['translation'],
8646
+ defaultNS: ['translation'],
8647
+ fallbackLng: ['dev'],
8648
+ fallbackNS: false,
8649
+ supportedLngs: false,
8650
+ nonExplicitSupportedLngs: false,
8651
+ load: 'all',
8652
+ preload: false,
8653
+ simplifyPluralSuffix: true,
8654
+ keySeparator: '.',
8655
+ nsSeparator: ':',
8656
+ pluralSeparator: '_',
8657
+ contextSeparator: '_',
8658
+ partialBundledLanguages: false,
8659
+ saveMissing: false,
8660
+ updateMissing: false,
8661
+ saveMissingTo: 'fallback',
8662
+ saveMissingPlurals: true,
8663
+ missingKeyHandler: false,
8664
+ missingInterpolationHandler: false,
8665
+ postProcess: false,
8666
+ postProcessPassResolved: false,
8667
+ returnNull: false,
8668
+ returnEmptyString: true,
8669
+ returnObjects: false,
8670
+ joinArrays: false,
8671
+ returnedObjectHandler: false,
8672
+ parseMissingKeyHandler: false,
8673
+ appendNamespaceToMissingKey: false,
8674
+ appendNamespaceToCIMode: false,
8675
+ overloadTranslationOptionHandler: args => {
8676
+ let ret = {};
8677
+ if (typeof args[1] === 'object') ret = args[1];
8678
+ if (isString(args[1])) ret.defaultValue = args[1];
8679
+ if (isString(args[2])) ret.tDescription = args[2];
8680
+ if (typeof args[2] === 'object' || typeof args[3] === 'object') {
8681
+ const options = args[3] || args[2];
8682
+ Object.keys(options).forEach(key => {
8683
+ ret[key] = options[key];
8684
+ });
8685
+ }
8686
+ return ret;
8687
+ },
8688
+ interpolation: {
8689
+ escapeValue: true,
8690
+ format: value => value,
8691
+ prefix: '{{',
8692
+ suffix: '}}',
8693
+ formatSeparator: ',',
8694
+ unescapePrefix: '-',
8695
+ nestingPrefix: '$t(',
8696
+ nestingSuffix: ')',
8697
+ nestingOptionsSeparator: ',',
8698
+ maxReplaces: 1000,
8699
+ skipOnVariables: true
8700
+ },
8701
+ cacheInBuiltFormats: true
8702
+ });
8703
+ const transformOptions = options => {
8704
+ if (isString(options.ns)) options.ns = [options.ns];
8705
+ if (isString(options.fallbackLng)) options.fallbackLng = [options.fallbackLng];
8706
+ if (isString(options.fallbackNS)) options.fallbackNS = [options.fallbackNS];
8707
+ if (options.supportedLngs?.indexOf?.('cimode') < 0) {
8708
+ options.supportedLngs = options.supportedLngs.concat(['cimode']);
8709
+ }
8710
+ if (typeof options.initImmediate === 'boolean') options.initAsync = options.initImmediate;
8711
+ return options;
8712
+ };
8713
+
8714
+ const noop = () => {};
8715
+ const bindMemberFunctions = inst => {
8716
+ const mems = Object.getOwnPropertyNames(Object.getPrototypeOf(inst));
8717
+ mems.forEach(mem => {
8718
+ if (typeof inst[mem] === 'function') {
8719
+ inst[mem] = inst[mem].bind(inst);
8720
+ }
8721
+ });
8722
+ };
8723
+ class I18n extends EventEmitter {
8724
+ constructor(options = {}, callback) {
8725
+ super();
8726
+ this.options = transformOptions(options);
8727
+ this.services = {};
8728
+ this.logger = baseLogger;
8729
+ this.modules = {
8730
+ external: []
8731
+ };
8732
+ bindMemberFunctions(this);
8733
+ if (callback && !this.isInitialized && !options.isClone) {
8734
+ if (!this.options.initAsync) {
8735
+ this.init(options, callback);
8736
+ return this;
8737
+ }
8738
+ setTimeout(() => {
8739
+ this.init(options, callback);
8740
+ }, 0);
8741
+ }
8742
+ }
8743
+ init(options = {}, callback) {
8744
+ this.isInitializing = true;
8745
+ if (typeof options === 'function') {
8746
+ callback = options;
8747
+ options = {};
8748
+ }
8749
+ if (options.defaultNS == null && options.ns) {
8750
+ if (isString(options.ns)) {
8751
+ options.defaultNS = options.ns;
8752
+ } else if (options.ns.indexOf('translation') < 0) {
8753
+ options.defaultNS = options.ns[0];
8754
+ }
8755
+ }
8756
+ const defOpts = get();
8757
+ this.options = {
8758
+ ...defOpts,
8759
+ ...this.options,
8760
+ ...transformOptions(options)
8761
+ };
8762
+ this.options.interpolation = {
8763
+ ...defOpts.interpolation,
8764
+ ...this.options.interpolation
8765
+ };
8766
+ if (options.keySeparator !== undefined) {
8767
+ this.options.userDefinedKeySeparator = options.keySeparator;
8768
+ }
8769
+ if (options.nsSeparator !== undefined) {
8770
+ this.options.userDefinedNsSeparator = options.nsSeparator;
8771
+ }
8772
+ if (typeof this.options.overloadTranslationOptionHandler !== 'function') {
8773
+ this.options.overloadTranslationOptionHandler = defOpts.overloadTranslationOptionHandler;
8774
+ }
8775
+ if (this.options.debug === true) {
8776
+ if (typeof console !== 'undefined') console.warn('i18next is maintained with support from locize.com — consider powering your project with managed localization (AI, CDN, integrations): https://locize.com');
8777
+ }
8778
+ const createClassOnDemand = ClassOrObject => {
8779
+ if (!ClassOrObject) return null;
8780
+ if (typeof ClassOrObject === 'function') return new ClassOrObject();
8781
+ return ClassOrObject;
8782
+ };
8783
+ if (!this.options.isClone) {
8784
+ if (this.modules.logger) {
8785
+ baseLogger.init(createClassOnDemand(this.modules.logger), this.options);
8786
+ } else {
8787
+ baseLogger.init(null, this.options);
8788
+ }
8789
+ let formatter;
8790
+ if (this.modules.formatter) {
8791
+ formatter = this.modules.formatter;
8792
+ } else {
8793
+ formatter = Formatter;
8794
+ }
8795
+ const lu = new LanguageUtil(this.options);
8796
+ this.store = new ResourceStore(this.options.resources, this.options);
8797
+ const s = this.services;
8798
+ s.logger = baseLogger;
8799
+ s.resourceStore = this.store;
8800
+ s.languageUtils = lu;
8801
+ s.pluralResolver = new PluralResolver(lu, {
8802
+ prepend: this.options.pluralSeparator,
8803
+ simplifyPluralSuffix: this.options.simplifyPluralSuffix
8804
+ });
8805
+ const usingLegacyFormatFunction = this.options.interpolation.format && this.options.interpolation.format !== defOpts.interpolation.format;
8806
+ if (usingLegacyFormatFunction) {
8807
+ this.logger.deprecate(`init: you are still using the legacy format function, please use the new approach: https://www.i18next.com/translation-function/formatting`);
8808
+ }
8809
+ if (formatter && (!this.options.interpolation.format || this.options.interpolation.format === defOpts.interpolation.format)) {
8810
+ s.formatter = createClassOnDemand(formatter);
8811
+ if (s.formatter.init) s.formatter.init(s, this.options);
8812
+ this.options.interpolation.format = s.formatter.format.bind(s.formatter);
8813
+ }
8814
+ s.interpolator = new Interpolator(this.options);
8815
+ s.utils = {
8816
+ hasLoadedNamespace: this.hasLoadedNamespace.bind(this)
8817
+ };
8818
+ s.backendConnector = new Connector(createClassOnDemand(this.modules.backend), s.resourceStore, s, this.options);
8819
+ s.backendConnector.on('*', (event, ...args) => {
8820
+ this.emit(event, ...args);
8821
+ });
8822
+ if (this.modules.languageDetector) {
8823
+ s.languageDetector = createClassOnDemand(this.modules.languageDetector);
8824
+ if (s.languageDetector.init) s.languageDetector.init(s, this.options.detection, this.options);
8825
+ }
8826
+ if (this.modules.i18nFormat) {
8827
+ s.i18nFormat = createClassOnDemand(this.modules.i18nFormat);
8828
+ if (s.i18nFormat.init) s.i18nFormat.init(this);
8829
+ }
8830
+ this.translator = new Translator(this.services, this.options);
8831
+ this.translator.on('*', (event, ...args) => {
8832
+ this.emit(event, ...args);
8833
+ });
8834
+ this.modules.external.forEach(m => {
8835
+ if (m.init) m.init(this);
8836
+ });
8837
+ }
8838
+ this.format = this.options.interpolation.format;
8839
+ if (!callback) callback = noop;
8840
+ if (this.options.fallbackLng && !this.services.languageDetector && !this.options.lng) {
8841
+ const codes = this.services.languageUtils.getFallbackCodes(this.options.fallbackLng);
8842
+ if (codes.length > 0 && codes[0] !== 'dev') this.options.lng = codes[0];
8843
+ }
8844
+ if (!this.services.languageDetector && !this.options.lng) {
8845
+ this.logger.warn('init: no languageDetector is used and no lng is defined');
8846
+ }
8847
+ const storeApi = ['getResource', 'hasResourceBundle', 'getResourceBundle', 'getDataByLanguage'];
8848
+ storeApi.forEach(fcName => {
8849
+ this[fcName] = (...args) => this.store[fcName](...args);
8850
+ });
8851
+ const storeApiChained = ['addResource', 'addResources', 'addResourceBundle', 'removeResourceBundle'];
8852
+ storeApiChained.forEach(fcName => {
8853
+ this[fcName] = (...args) => {
8854
+ this.store[fcName](...args);
8855
+ return this;
8856
+ };
8857
+ });
8858
+ const deferred = defer();
8859
+ const load = () => {
8860
+ const finish = (err, t) => {
8861
+ this.isInitializing = false;
8862
+ if (this.isInitialized && !this.initializedStoreOnce) this.logger.warn('init: i18next is already initialized. You should call init just once!');
8863
+ this.isInitialized = true;
8864
+ if (!this.options.isClone) this.logger.log('initialized', this.options);
8865
+ this.emit('initialized', this.options);
8866
+ deferred.resolve(t);
8867
+ callback(err, t);
8868
+ };
8869
+ if (this.languages && !this.isInitialized) return finish(null, this.t.bind(this));
8870
+ this.changeLanguage(this.options.lng, finish);
8871
+ };
8872
+ if (this.options.resources || !this.options.initAsync) {
8873
+ load();
8874
+ } else {
8875
+ setTimeout(load, 0);
8876
+ }
8877
+ return deferred;
8878
+ }
8879
+ loadResources(language, callback = noop) {
8880
+ let usedCallback = callback;
8881
+ const usedLng = isString(language) ? language : this.language;
8882
+ if (typeof language === 'function') usedCallback = language;
8883
+ if (!this.options.resources || this.options.partialBundledLanguages) {
8884
+ if (usedLng?.toLowerCase() === 'cimode' && (!this.options.preload || this.options.preload.length === 0)) return usedCallback();
8885
+ const toLoad = [];
8886
+ const append = lng => {
8887
+ if (!lng) return;
8888
+ if (lng === 'cimode') return;
8889
+ const lngs = this.services.languageUtils.toResolveHierarchy(lng);
8890
+ lngs.forEach(l => {
8891
+ if (l === 'cimode') return;
8892
+ if (toLoad.indexOf(l) < 0) toLoad.push(l);
8893
+ });
8894
+ };
8895
+ if (!usedLng) {
8896
+ const fallbacks = this.services.languageUtils.getFallbackCodes(this.options.fallbackLng);
8897
+ fallbacks.forEach(l => append(l));
8898
+ } else {
8899
+ append(usedLng);
8900
+ }
8901
+ this.options.preload?.forEach?.(l => append(l));
8902
+ this.services.backendConnector.load(toLoad, this.options.ns, e => {
8903
+ if (!e && !this.resolvedLanguage && this.language) this.setResolvedLanguage(this.language);
8904
+ usedCallback(e);
8905
+ });
8906
+ } else {
8907
+ usedCallback(null);
8908
+ }
8909
+ }
8910
+ reloadResources(lngs, ns, callback) {
8911
+ const deferred = defer();
8912
+ if (typeof lngs === 'function') {
8913
+ callback = lngs;
8914
+ lngs = undefined;
8915
+ }
8916
+ if (typeof ns === 'function') {
8917
+ callback = ns;
8918
+ ns = undefined;
8919
+ }
8920
+ if (!lngs) lngs = this.languages;
8921
+ if (!ns) ns = this.options.ns;
8922
+ if (!callback) callback = noop;
8923
+ this.services.backendConnector.reload(lngs, ns, err => {
8924
+ deferred.resolve();
8925
+ callback(err);
8926
+ });
8927
+ return deferred;
8928
+ }
8929
+ use(module) {
8930
+ if (!module) throw new Error('You are passing an undefined module! Please check the object you are passing to i18next.use()');
8931
+ if (!module.type) throw new Error('You are passing a wrong module! Please check the object you are passing to i18next.use()');
8932
+ if (module.type === 'backend') {
8933
+ this.modules.backend = module;
8934
+ }
8935
+ if (module.type === 'logger' || module.log && module.warn && module.error) {
8936
+ this.modules.logger = module;
8937
+ }
8938
+ if (module.type === 'languageDetector') {
8939
+ this.modules.languageDetector = module;
8940
+ }
8941
+ if (module.type === 'i18nFormat') {
8942
+ this.modules.i18nFormat = module;
8943
+ }
8944
+ if (module.type === 'postProcessor') {
8945
+ postProcessor.addPostProcessor(module);
8946
+ }
8947
+ if (module.type === 'formatter') {
8948
+ this.modules.formatter = module;
8949
+ }
8950
+ if (module.type === '3rdParty') {
8951
+ this.modules.external.push(module);
8952
+ }
8953
+ return this;
8954
+ }
8955
+ setResolvedLanguage(l) {
8956
+ if (!l || !this.languages) return;
8957
+ if (['cimode', 'dev'].indexOf(l) > -1) return;
8958
+ for (let li = 0; li < this.languages.length; li++) {
8959
+ const lngInLngs = this.languages[li];
8960
+ if (['cimode', 'dev'].indexOf(lngInLngs) > -1) continue;
8961
+ if (this.store.hasLanguageSomeTranslations(lngInLngs)) {
8962
+ this.resolvedLanguage = lngInLngs;
8963
+ break;
8964
+ }
8965
+ }
8966
+ if (!this.resolvedLanguage && this.languages.indexOf(l) < 0 && this.store.hasLanguageSomeTranslations(l)) {
8967
+ this.resolvedLanguage = l;
8968
+ this.languages.unshift(l);
8969
+ }
8970
+ }
8971
+ changeLanguage(lng, callback) {
8972
+ this.isLanguageChangingTo = lng;
8973
+ const deferred = defer();
8974
+ this.emit('languageChanging', lng);
8975
+ const setLngProps = l => {
8976
+ this.language = l;
8977
+ this.languages = this.services.languageUtils.toResolveHierarchy(l);
8978
+ this.resolvedLanguage = undefined;
8979
+ this.setResolvedLanguage(l);
8980
+ };
8981
+ const done = (err, l) => {
8982
+ if (l) {
8983
+ if (this.isLanguageChangingTo === lng) {
8984
+ setLngProps(l);
8985
+ this.translator.changeLanguage(l);
8986
+ this.isLanguageChangingTo = undefined;
8987
+ this.emit('languageChanged', l);
8988
+ this.logger.log('languageChanged', l);
8989
+ }
8990
+ } else {
8991
+ this.isLanguageChangingTo = undefined;
8992
+ }
8993
+ deferred.resolve((...args) => this.t(...args));
8994
+ if (callback) callback(err, (...args) => this.t(...args));
8995
+ };
8996
+ const setLng = lngs => {
8997
+ if (!lng && !lngs && this.services.languageDetector) lngs = [];
8998
+ const fl = isString(lngs) ? lngs : lngs && lngs[0];
8999
+ const l = this.store.hasLanguageSomeTranslations(fl) ? fl : this.services.languageUtils.getBestMatchFromCodes(isString(lngs) ? [lngs] : lngs);
9000
+ if (l) {
9001
+ if (!this.language) {
9002
+ setLngProps(l);
9003
+ }
9004
+ if (!this.translator.language) this.translator.changeLanguage(l);
9005
+ this.services.languageDetector?.cacheUserLanguage?.(l);
9006
+ }
9007
+ this.loadResources(l, err => {
9008
+ done(err, l);
9009
+ });
9010
+ };
9011
+ if (!lng && this.services.languageDetector && !this.services.languageDetector.async) {
9012
+ setLng(this.services.languageDetector.detect());
9013
+ } else if (!lng && this.services.languageDetector && this.services.languageDetector.async) {
9014
+ if (this.services.languageDetector.detect.length === 0) {
9015
+ this.services.languageDetector.detect().then(setLng);
9016
+ } else {
9017
+ this.services.languageDetector.detect(setLng);
9018
+ }
9019
+ } else {
9020
+ setLng(lng);
9021
+ }
9022
+ return deferred;
9023
+ }
9024
+ getFixedT(lng, ns, keyPrefix) {
9025
+ const fixedT = (key, opts, ...rest) => {
9026
+ let o;
9027
+ if (typeof opts !== 'object') {
9028
+ o = this.options.overloadTranslationOptionHandler([key, opts].concat(rest));
9029
+ } else {
9030
+ o = {
9031
+ ...opts
9032
+ };
9033
+ }
9034
+ o.lng = o.lng || fixedT.lng;
9035
+ o.lngs = o.lngs || fixedT.lngs;
9036
+ o.ns = o.ns || fixedT.ns;
9037
+ if (o.keyPrefix !== '') o.keyPrefix = o.keyPrefix || keyPrefix || fixedT.keyPrefix;
9038
+ const keySeparator = this.options.keySeparator || '.';
9039
+ let resultKey;
9040
+ if (o.keyPrefix && Array.isArray(key)) {
9041
+ resultKey = key.map(k => {
9042
+ if (typeof k === 'function') k = keysFromSelector(k, {
9043
+ ...this.options,
9044
+ ...opts
9045
+ });
9046
+ return `${o.keyPrefix}${keySeparator}${k}`;
9047
+ });
9048
+ } else {
9049
+ if (typeof key === 'function') key = keysFromSelector(key, {
9050
+ ...this.options,
9051
+ ...opts
9052
+ });
9053
+ resultKey = o.keyPrefix ? `${o.keyPrefix}${keySeparator}${key}` : key;
9054
+ }
9055
+ return this.t(resultKey, o);
9056
+ };
9057
+ if (isString(lng)) {
9058
+ fixedT.lng = lng;
9059
+ } else {
9060
+ fixedT.lngs = lng;
9061
+ }
9062
+ fixedT.ns = ns;
9063
+ fixedT.keyPrefix = keyPrefix;
9064
+ return fixedT;
9065
+ }
9066
+ t(...args) {
9067
+ return this.translator?.translate(...args);
9068
+ }
9069
+ exists(...args) {
9070
+ return this.translator?.exists(...args);
9071
+ }
9072
+ setDefaultNamespace(ns) {
9073
+ this.options.defaultNS = ns;
9074
+ }
9075
+ hasLoadedNamespace(ns, options = {}) {
9076
+ if (!this.isInitialized) {
9077
+ this.logger.warn('hasLoadedNamespace: i18next was not initialized', this.languages);
9078
+ return false;
9079
+ }
9080
+ if (!this.languages || !this.languages.length) {
9081
+ this.logger.warn('hasLoadedNamespace: i18n.languages were undefined or empty', this.languages);
9082
+ return false;
9083
+ }
9084
+ const lng = options.lng || this.resolvedLanguage || this.languages[0];
9085
+ const fallbackLng = this.options ? this.options.fallbackLng : false;
9086
+ const lastLng = this.languages[this.languages.length - 1];
9087
+ if (lng.toLowerCase() === 'cimode') return true;
9088
+ const loadNotPending = (l, n) => {
9089
+ const loadState = this.services.backendConnector.state[`${l}|${n}`];
9090
+ return loadState === -1 || loadState === 0 || loadState === 2;
9091
+ };
9092
+ if (options.precheck) {
9093
+ const preResult = options.precheck(this, loadNotPending);
9094
+ if (preResult !== undefined) return preResult;
9095
+ }
9096
+ if (this.hasResourceBundle(lng, ns)) return true;
9097
+ if (!this.services.backendConnector.backend || this.options.resources && !this.options.partialBundledLanguages) return true;
9098
+ if (loadNotPending(lng, ns) && (!fallbackLng || loadNotPending(lastLng, ns))) return true;
9099
+ return false;
9100
+ }
9101
+ loadNamespaces(ns, callback) {
9102
+ const deferred = defer();
9103
+ if (!this.options.ns) {
9104
+ if (callback) callback();
9105
+ return Promise.resolve();
9106
+ }
9107
+ if (isString(ns)) ns = [ns];
9108
+ ns.forEach(n => {
9109
+ if (this.options.ns.indexOf(n) < 0) this.options.ns.push(n);
9110
+ });
9111
+ this.loadResources(err => {
9112
+ deferred.resolve();
9113
+ if (callback) callback(err);
9114
+ });
9115
+ return deferred;
9116
+ }
9117
+ loadLanguages(lngs, callback) {
9118
+ const deferred = defer();
9119
+ if (isString(lngs)) lngs = [lngs];
9120
+ const preloaded = this.options.preload || [];
9121
+ const newLngs = lngs.filter(lng => preloaded.indexOf(lng) < 0 && this.services.languageUtils.isSupportedCode(lng));
9122
+ if (!newLngs.length) {
9123
+ if (callback) callback();
9124
+ return Promise.resolve();
9125
+ }
9126
+ this.options.preload = preloaded.concat(newLngs);
9127
+ this.loadResources(err => {
9128
+ deferred.resolve();
9129
+ if (callback) callback(err);
9130
+ });
9131
+ return deferred;
9132
+ }
9133
+ dir(lng) {
9134
+ if (!lng) lng = this.resolvedLanguage || (this.languages?.length > 0 ? this.languages[0] : this.language);
9135
+ if (!lng) return 'rtl';
9136
+ try {
9137
+ const l = new Intl.Locale(lng);
9138
+ if (l && l.getTextInfo) {
9139
+ const ti = l.getTextInfo();
9140
+ if (ti && ti.direction) return ti.direction;
9141
+ }
9142
+ } catch (e) {}
9143
+ const rtlLngs = ['ar', 'shu', 'sqr', 'ssh', 'xaa', 'yhd', 'yud', 'aao', 'abh', 'abv', 'acm', 'acq', 'acw', 'acx', 'acy', 'adf', 'ads', 'aeb', 'aec', 'afb', 'ajp', 'apc', 'apd', 'arb', 'arq', 'ars', 'ary', 'arz', 'auz', 'avl', 'ayh', 'ayl', 'ayn', 'ayp', 'bbz', 'pga', 'he', 'iw', 'ps', 'pbt', 'pbu', 'pst', 'prp', 'prd', 'ug', 'ur', 'ydd', 'yds', 'yih', 'ji', 'yi', 'hbo', 'men', 'xmn', 'fa', 'jpr', 'peo', 'pes', 'prs', 'dv', 'sam', 'ckb'];
9144
+ const languageUtils = this.services?.languageUtils || new LanguageUtil(get());
9145
+ if (lng.toLowerCase().indexOf('-latn') > 1) return 'ltr';
9146
+ return rtlLngs.indexOf(languageUtils.getLanguagePartFromCode(lng)) > -1 || lng.toLowerCase().indexOf('-arab') > 1 ? 'rtl' : 'ltr';
9147
+ }
9148
+ static createInstance(options = {}, callback) {
9149
+ const instance = new I18n(options, callback);
9150
+ instance.createInstance = I18n.createInstance;
9151
+ return instance;
9152
+ }
9153
+ cloneInstance(options = {}, callback = noop) {
9154
+ const forkResourceStore = options.forkResourceStore;
9155
+ if (forkResourceStore) delete options.forkResourceStore;
9156
+ const mergedOptions = {
9157
+ ...this.options,
9158
+ ...options,
9159
+ ...{
9160
+ isClone: true
9161
+ }
9162
+ };
9163
+ const clone = new I18n(mergedOptions);
9164
+ if (options.debug !== undefined || options.prefix !== undefined) {
9165
+ clone.logger = clone.logger.clone(options);
9166
+ }
9167
+ const membersToCopy = ['store', 'services', 'language'];
9168
+ membersToCopy.forEach(m => {
9169
+ clone[m] = this[m];
9170
+ });
9171
+ clone.services = {
9172
+ ...this.services
9173
+ };
9174
+ clone.services.utils = {
9175
+ hasLoadedNamespace: clone.hasLoadedNamespace.bind(clone)
9176
+ };
9177
+ if (forkResourceStore) {
9178
+ const clonedData = Object.keys(this.store.data).reduce((prev, l) => {
9179
+ prev[l] = {
9180
+ ...this.store.data[l]
6888
9181
  };
9182
+ prev[l] = Object.keys(prev[l]).reduce((acc, n) => {
9183
+ acc[n] = {
9184
+ ...prev[l][n]
9185
+ };
9186
+ return acc;
9187
+ }, prev[l]);
9188
+ return prev;
9189
+ }, {});
9190
+ clone.store = new ResourceStore(clonedData, mergedOptions);
9191
+ clone.services.resourceStore = clone.store;
9192
+ }
9193
+ if (options.interpolation) {
9194
+ const defOpts = get();
9195
+ const mergedInterpolation = {
9196
+ ...defOpts.interpolation,
9197
+ ...this.options.interpolation,
9198
+ ...options.interpolation
9199
+ };
9200
+ const mergedForInterpolator = {
9201
+ ...mergedOptions,
9202
+ interpolation: mergedInterpolation
9203
+ };
9204
+ clone.services.interpolator = new Interpolator(mergedForInterpolator);
6889
9205
  }
9206
+ clone.translator = new Translator(clone.services, mergedOptions);
9207
+ clone.translator.on('*', (event, ...args) => {
9208
+ clone.emit(event, ...args);
9209
+ });
9210
+ clone.init(mergedOptions, callback);
9211
+ clone.translator.options = mergedOptions;
9212
+ clone.translator.backendConnector.services.utils = {
9213
+ hasLoadedNamespace: clone.hasLoadedNamespace.bind(clone)
9214
+ };
9215
+ return clone;
9216
+ }
9217
+ toJSON() {
9218
+ return {
9219
+ options: this.options,
9220
+ store: this.store,
9221
+ language: this.language,
9222
+ languages: this.languages,
9223
+ resolvedLanguage: this.resolvedLanguage
9224
+ };
9225
+ }
6890
9226
  }
9227
+ const instance = I18n.createInstance();
6891
9228
 
6892
- var styleService = /*#__PURE__*/Object.freeze({
6893
- __proto__: null,
6894
- cleanupOldStyles: cleanupOldStyles,
6895
- deleteStyleById: deleteStyleById,
6896
- deleteStyles: deleteStyles,
6897
- downloadStyleWithProvider: downloadStyleWithProvider,
6898
- downloadStyles: downloadStyles,
6899
- getStyleStats: getStyleStats,
6900
- isStyleDownloaded: isStyleDownloaded,
6901
- loadStyleById: loadStyleById,
6902
- loadStyles: loadStyles
6903
- });
9229
+ instance.createInstance;
9230
+ instance.dir;
9231
+ instance.init;
9232
+ instance.loadResources;
9233
+ instance.reloadResources;
9234
+ instance.use;
9235
+ instance.changeLanguage;
9236
+ instance.getFixedT;
9237
+ instance.t;
9238
+ instance.exists;
9239
+ instance.setDefaultNamespace;
9240
+ instance.hasLoadedNamespace;
9241
+ instance.loadNamespaces;
9242
+ instance.loadLanguages;
6904
9243
 
6905
9244
  /**
6906
- * Simplified theme system for dark/light/system mode management
6907
- * Now that all components use Tailwind CSS, we only need basic theme switching
9245
+ * English translations for map-gl-offline
6908
9246
  */
6909
- const lightTheme = {
6910
- mode: 'light',
6911
- preference: 'light',
9247
+ const en = {
9248
+ // General
9249
+ 'app.title': 'Offline Manager',
9250
+ 'app.close': 'Close',
9251
+ 'app.cancel': 'Cancel',
9252
+ 'app.save': 'Save',
9253
+ 'app.delete': 'Delete',
9254
+ 'app.confirm': 'Confirm',
9255
+ 'app.ok': 'OK',
9256
+ 'app.loading': 'Loading...',
9257
+ 'app.error': 'Error',
9258
+ 'app.success': 'Success',
9259
+ 'app.warning': 'Warning',
9260
+ 'app.refresh': 'Refresh',
9261
+ 'app.details': 'Details',
9262
+ 'app.focus': 'Focus',
9263
+ // Theme
9264
+ 'theme.toggle': 'Toggle theme',
9265
+ 'theme.light': 'Light',
9266
+ 'theme.dark': 'Dark',
9267
+ // Language
9268
+ 'language.select': 'Select language',
9269
+ 'language.en': 'English',
9270
+ 'language.ar': 'Arabic',
9271
+ // Header
9272
+ 'header.title': 'Offline Manager',
9273
+ 'header.subtitle': '{{count}} regions • {{size}} total',
9274
+ // Actions
9275
+ 'actions.addRegion': 'Add Region',
9276
+ 'actions.refresh': 'Refresh',
9277
+ 'actions.loadStyle': 'Load Style',
9278
+ 'actions.fixTiles': 'Fix Tiles',
9279
+ 'actions.deleteStyle': 'Delete Style',
9280
+ 'actions.redownload': 'Re-download',
9281
+ 'actions.importExport': 'Import/Export',
9282
+ // Region Form
9283
+ 'regionForm.title': 'Download Offline Region',
9284
+ 'regionForm.subtitle': 'Selected area: {{area}} km²',
9285
+ 'regionForm.name': 'Region Name',
9286
+ 'regionForm.namePlaceholder': 'e.g., Downtown District',
9287
+ 'regionForm.minZoom': 'Min Zoom',
9288
+ 'regionForm.maxZoom': 'Max Zoom',
9289
+ 'regionForm.styleUrl': 'Style URL',
9290
+ 'regionForm.provider': 'Style Provider',
9291
+ 'regionForm.providerAuto': 'Auto-detect',
9292
+ 'regionForm.providerMapbox': 'Mapbox GL (requires access token)',
9293
+ 'regionForm.providerMaplibre': 'MapLibre GL (open source)',
9294
+ 'regionForm.providerInfo': 'Auto-detect will analyze the style URL to determine the provider',
9295
+ 'regionForm.accessToken': 'Mapbox Access Token',
9296
+ 'regionForm.accessTokenRequired': 'Required',
9297
+ 'regionForm.accessTokenHelp': 'Get your access token from',
9298
+ 'regionForm.downloadRegion': 'Download Region',
9299
+ 'regionForm.area': 'Area',
9300
+ 'regionForm.bounds': 'Bounds',
9301
+ // Region List
9302
+ 'regionList.empty': 'No offline styles or regions found. Click "Add Region" to get started.',
9303
+ 'regionList.emptyFallback': 'No offline regions found. Click "Add Region" to get started.',
9304
+ 'regionList.orphanedHeader': 'Regions without Style',
9305
+ 'regionList.orphanedDescription': '{{count}} region(s) not associated with any style',
9306
+ 'regionList.zoom': 'Zoom',
9307
+ 'regionList.downloaded': 'Downloaded',
9308
+ 'regionList.size': 'Size',
9309
+ 'regionList.styleId': 'Style ID',
9310
+ 'regionList.storedStyleSize': 'Stored Style Size',
9311
+ // Region Details
9312
+ 'regionDetails.title': 'Region Details',
9313
+ 'regionDetails.focusOnMap': 'Focus on Map',
9314
+ // Style
9315
+ 'style.unnamedStyle': 'Unnamed Style',
9316
+ 'style.regions': '{{count}} region(s)',
9317
+ // Delete Confirmation
9318
+ 'delete.regionTitle': 'Delete Region',
9319
+ 'delete.regionMessage': 'Are you sure you want to delete the region "{{name}}"? This action cannot be undone.',
9320
+ 'delete.styleTitle': 'Delete Style',
9321
+ 'delete.styleMessage': 'Are you sure you want to delete the style "{{name}}"? This action cannot be undone and will affect associated regions.',
9322
+ // Re-download
9323
+ 'redownload.title': 'Re-download Region',
9324
+ 'redownload.message': 'Re-download "{{name}}"?\n\nThis will:\n• Delete existing tiles and resources\n• Re-download all data with current settings\n• Fix any corrupted or outdated resources',
9325
+ 'redownload.button': 'Re-download',
9326
+ // Fix Tiles
9327
+ 'fixTiles.noIssuesTitle': 'No Issues Found',
9328
+ 'fixTiles.noIssuesMessage': 'No compressed tiles detected. Your tiles are already in the correct format!',
9329
+ 'fixTiles.title': 'Fix Compressed Tiles',
9330
+ 'fixTiles.message': "Found {{count}} compressed tiles that may cause rendering errors.\n\nThis will:\n1. Remove all compressed tiles ({{count}} tiles)\n2. You'll need to re-download regions to get properly decompressed tiles\n\nContinue?",
9331
+ 'fixTiles.button': 'Fix Tiles',
9332
+ 'fixTiles.completeTitle': 'Cleanup Complete',
9333
+ 'fixTiles.completeMessage': 'Successfully removed {{count}} compressed tiles.\n\nPlease re-download your regions to get the fixed tiles.',
9334
+ // Download Progress
9335
+ 'download.active': 'Active Downloads',
9336
+ 'download.phase.style': 'Style',
9337
+ 'download.phase.sprites': 'Sprites',
9338
+ 'download.phase.glyphs': 'Glyphs',
9339
+ 'download.phase.tiles': 'Tiles',
9340
+ // Style Selection
9341
+ 'styleSelection.title': 'Select Offline Style',
9342
+ 'styleSelection.message': 'Choose which offline style to load:',
9343
+ 'styleSelection.sources': 'sources',
9344
+ // Import/Export
9345
+ 'importExport.title': 'Import/Export',
9346
+ 'importExport.export': 'Export',
9347
+ 'importExport.import': 'Import',
9348
+ // Errors
9349
+ 'error.loadingContent': 'Error loading content',
9350
+ 'error.tryAgain': 'Please try again',
9351
+ 'error.mapNotInitialized': 'Map is not initialized. Please ensure the map is loaded before loading a style.',
9352
+ 'error.invalidStyleData': 'Invalid style data. The style may be corrupted.',
9353
+ 'error.styleMissingSources': 'Style has no sources defined. The style may be incomplete.',
9354
+ 'error.styleMissingLayers': 'Style has no layers defined. The style may be incomplete.',
9355
+ 'error.regionNotFound': 'Region not found',
9356
+ 'error.downloadFailed': 'Download failed',
9357
+ 'error.deleteFailed': 'Failed to delete',
9358
+ 'error.loadStyleFailed': 'Failed to load style',
9359
+ // Warnings
9360
+ 'warning.compressedTiles': 'Found {{count}} compressed tiles that may cause rendering issues.',
9361
+ 'warning.compressedTilesRecommendation': 'Recommended: Use the "Re-download" button to fix this issue.',
9362
+ 'warning.continueAnyway': 'Do you want to continue loading the style anyway?',
9363
+ // Drawing Tool
9364
+ 'drawing.instructions': 'Click and drag to draw a region',
9365
+ 'drawing.cancel': 'Press ESC to cancel',
9366
+ // Offline Maps Button
9367
+ 'button.offlineMaps': 'Offline Maps',
9368
+ // Region Details Modal
9369
+ 'regionDetails.bounds': 'Bounds',
9370
+ 'regionDetails.zoomRange': 'Zoom Range',
9371
+ 'regionDetails.created': 'Created',
9372
+ // Import/Export Modal
9373
+ 'importExport.regionTitle': 'Import/Export Region',
9374
+ 'importExport.regionInfo': 'Region Information',
9375
+ 'importExport.id': 'ID',
9376
+ 'importExport.name': 'Name',
9377
+ 'importExport.unnamed': 'Unnamed',
9378
+ 'importExport.zoom': 'Zoom',
9379
+ 'importExport.created': 'Created',
9380
+ 'importExport.exportRegion': 'Export Region',
9381
+ 'importExport.exportFormat': 'Export Format',
9382
+ 'importExport.formatJson': 'JSON - Complete data (recommended)',
9383
+ 'importExport.formatPmtiles': 'PMTiles - Web optimized tiles',
9384
+ 'importExport.formatMbtiles': 'MBTiles - Industry standard',
9385
+ 'importExport.formatHint': 'Choose format based on your use case',
9386
+ 'importExport.includeComponents': 'Include Components',
9387
+ 'importExport.styleConfig': 'Style Configuration',
9388
+ 'importExport.mapTiles': 'Map Tiles',
9389
+ 'importExport.spritesIcons': 'Sprites & Icons',
9390
+ 'importExport.fontsGlyphs': 'Fonts & Glyphs',
9391
+ 'importExport.preparingExport': 'Preparing export...',
9392
+ 'importExport.exportComplete': 'Export complete!',
9393
+ 'importExport.exportFailed': 'Export failed. Please try again.',
9394
+ 'importExport.importRegion': 'Import Region',
9395
+ 'importExport.selectFile': 'Select File',
9396
+ 'importExport.fileFormatsHint': 'Supports JSON, PMTiles, and MBTiles formats',
9397
+ 'importExport.newRegionName': 'New Region Name (Optional)',
9398
+ 'importExport.newRegionNamePlaceholder': 'Leave empty to use original name',
9399
+ 'importExport.overwriteIfExists': 'Overwrite if region exists',
9400
+ 'importExport.preparingImport': 'Preparing import...',
9401
+ 'importExport.importComplete': 'Import complete!',
9402
+ 'importExport.importFailed': 'Import failed. Please try again.',
9403
+ 'importExport.formatGuide': 'Format Guide',
9404
+ 'importExport.jsonDesc': 'Complete data, human-readable, best for development',
9405
+ 'importExport.pmtilesDesc': 'Web-optimized, efficient serving, cloud-friendly',
9406
+ 'importExport.mbtilesDesc': 'Industry standard, SQLite-based, cross-platform',
9407
+ // Active Downloads
9408
+ 'download.activeCount': 'Active Downloads ({{count}})',
9409
+ // Panel Manager additional strings
9410
+ 'panel.downloadedRegions': 'Downloaded Regions',
9411
+ 'panel.noExpiry': 'No expiry',
9412
+ 'panel.noBounds': 'No bounds',
9413
+ 'panel.sources': 'sources',
9414
+ // Empty/Error States
9415
+ 'emptyState.title': 'No items',
9416
+ 'emptyState.message': 'There are no items to display',
9417
+ 'errorState.message': 'Something went wrong',
9418
+ 'errorState.retry': 'Try Again',
9419
+ // List
9420
+ 'list.empty': 'No items to display',
9421
+ 'list.noItemsFound': 'No items found',
9422
+ // Toast
9423
+ 'toast.dismiss': 'Dismiss notification',
9424
+ // Control Buttons
9425
+ 'control.offlineMapManager': 'Offline Map Manager',
9426
+ 'control.cancelSelection': 'Cancel Selection',
9427
+ 'control.saveSelectedRegion': 'Save Selected Region',
9428
+ // Alerts/Messages
9429
+ 'alert.unknownError': 'Unknown error occurred',
9430
+ 'alert.downloadNotImplemented': 'Download feature not implemented in this refactored version',
9431
+ 'alert.mapNotAvailable': 'Map not available',
9432
+ 'alert.invalidBounds': 'Invalid bounds for region',
9433
+ 'alert.styleNotFound': 'Style not found',
9434
+ 'alert.noStylesFound': 'No styles found in IndexedDB',
9435
+ 'alert.failedToApplyStyle': 'Failed to apply style to map',
9436
+ 'alert.failedToLoadStyle': 'Failed to load style',
9437
+ 'alert.failedToRedownload': 'Failed to re-download region',
9438
+ 'alert.downloadManagerNotAvailable': 'Download manager not available',
9439
+ // Download Manager
9440
+ 'downloadManager.downloadingStyle': 'Downloading style',
9441
+ 'downloadManager.downloadingSprites': 'Downloading sprites',
9442
+ 'downloadManager.downloadingGlyphs': 'Downloading glyphs',
9443
+ 'downloadManager.downloadingTiles': 'Downloading tiles',
9444
+ 'downloadManager.styleExistsNotFound': 'Style exists but could not be found',
9445
+ 'downloadManager.styleNotFoundForResources': 'Style not found for resource download',
9446
+ 'downloadManager.styleNotFoundForTiles': 'Style not found for tile download',
9447
+ 'downloadManager.styleNoSources': 'Style does not contain any sources for tile download',
9448
+ // Form Validation
9449
+ 'validation.required': 'This field is required',
9450
+ 'validation.invalidFormat': 'Invalid format',
9451
+ 'validation.mustBeNumber': 'Must be a number',
9452
+ 'validation.mustBeWholeNumber': 'Must be a whole number',
9453
+ 'validation.invalidUrl': 'Invalid URL format',
6912
9454
  };
6913
- // Theme context management
6914
- class ThemeManager {
6915
- currentTheme = lightTheme;
6916
- listeners = [];
6917
- constructor() {
6918
- // Initialize theme from localStorage or system preference
6919
- this.initializeTheme();
6920
- }
6921
- initializeTheme() {
6922
- const savedPreference = localStorage.getItem('offline-manager-theme');
6923
- if (savedPreference === 'system' || !savedPreference) {
6924
- // Use system preference
6925
- this.setThemePreference('system');
6926
- }
6927
- else {
6928
- this.setThemePreference(savedPreference);
6929
- }
6930
- // Listen for system theme changes
6931
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
6932
- if (this.currentTheme.preference === 'system') {
6933
- this.applyThemeMode(e.matches ? 'dark' : 'light', 'system');
6934
- }
6935
- });
6936
- }
6937
- getTheme() {
6938
- return this.currentTheme;
6939
- }
9455
+
9456
+ /**
9457
+ * Arabic translations for map-gl-offline
9458
+ * الترجمة العربية لـ map-gl-offline
9459
+ */
9460
+ const ar = {
9461
+ // General - عام
9462
+ 'app.title': 'مدير الخرائط غير المتصلة',
9463
+ 'app.close': 'إغلاق',
9464
+ 'app.cancel': 'إلغاء',
9465
+ 'app.save': 'حفظ',
9466
+ 'app.delete': 'حذف',
9467
+ 'app.confirm': 'تأكيد',
9468
+ 'app.ok': 'موافق',
9469
+ 'app.loading': 'جاري التحميل...',
9470
+ 'app.error': 'خطأ',
9471
+ 'app.success': 'نجاح',
9472
+ 'app.warning': 'تحذير',
9473
+ 'app.refresh': 'تحديث',
9474
+ 'app.details': 'التفاصيل',
9475
+ 'app.focus': 'التركيز',
9476
+ // Theme - السمة
9477
+ 'theme.toggle': 'تبديل السمة',
9478
+ 'theme.light': 'فاتح',
9479
+ 'theme.dark': 'داكن',
9480
+ // Language - اللغة
9481
+ 'language.select': 'اختر اللغة',
9482
+ 'language.en': 'الإنجليزية',
9483
+ 'language.ar': 'العربية',
9484
+ // Header - الرأس
9485
+ 'header.title': 'مدير الخرائط غير المتصلة',
9486
+ 'header.subtitle': '{{count}} منطقة • {{size}} إجمالي',
9487
+ // Actions - الإجراءات
9488
+ 'actions.addRegion': 'إضافة منطقة',
9489
+ 'actions.refresh': 'تحديث',
9490
+ 'actions.loadStyle': 'تحميل النمط',
9491
+ 'actions.fixTiles': 'إصلاح البلاطات',
9492
+ 'actions.deleteStyle': 'حذف النمط',
9493
+ 'actions.redownload': 'إعادة التحميل',
9494
+ 'actions.importExport': 'استيراد/تصدير',
9495
+ // Region Form - نموذج المنطقة
9496
+ 'regionForm.title': 'تحميل منطقة غير متصلة',
9497
+ 'regionForm.subtitle': 'المساحة المحددة: {{area}} كم²',
9498
+ 'regionForm.name': 'اسم المنطقة',
9499
+ 'regionForm.namePlaceholder': 'مثال: وسط المدينة',
9500
+ 'regionForm.minZoom': 'أدنى تكبير',
9501
+ 'regionForm.maxZoom': 'أقصى تكبير',
9502
+ 'regionForm.styleUrl': 'رابط النمط',
9503
+ 'regionForm.provider': 'مزود النمط',
9504
+ 'regionForm.providerAuto': 'اكتشاف تلقائي',
9505
+ 'regionForm.providerMapbox': 'Mapbox GL (يتطلب رمز الوصول)',
9506
+ 'regionForm.providerMaplibre': 'MapLibre GL (مفتوح المصدر)',
9507
+ 'regionForm.providerInfo': 'سيقوم الاكتشاف التلقائي بتحليل رابط النمط لتحديد المزود',
9508
+ 'regionForm.accessToken': 'رمز وصول Mapbox',
9509
+ 'regionForm.accessTokenRequired': 'مطلوب',
9510
+ 'regionForm.accessTokenHelp': 'احصل على رمز الوصول من',
9511
+ 'regionForm.downloadRegion': 'تحميل المنطقة',
9512
+ 'regionForm.area': 'المساحة',
9513
+ 'regionForm.bounds': 'الحدود',
9514
+ // Region List - قائمة المناطق
9515
+ 'regionList.empty': 'لم يتم العثور على أنماط أو مناطق غير متصلة. انقر على "إضافة منطقة" للبدء.',
9516
+ 'regionList.emptyFallback': 'لم يتم العثور على مناطق غير متصلة. انقر على "إضافة منطقة" للبدء.',
9517
+ 'regionList.orphanedHeader': 'مناطق بدون نمط',
9518
+ 'regionList.orphanedDescription': '{{count}} منطقة غير مرتبطة بأي نمط',
9519
+ 'regionList.zoom': 'التكبير',
9520
+ 'regionList.downloaded': 'تم التحميل',
9521
+ 'regionList.size': 'الحجم',
9522
+ 'regionList.styleId': 'معرف النمط',
9523
+ 'regionList.storedStyleSize': 'حجم النمط المخزن',
9524
+ // Region Details - تفاصيل المنطقة
9525
+ 'regionDetails.title': 'تفاصيل المنطقة',
9526
+ 'regionDetails.focusOnMap': 'التركيز على الخريطة',
9527
+ // Style - النمط
9528
+ 'style.unnamedStyle': 'نمط بدون اسم',
9529
+ 'style.regions': '{{count}} منطقة',
9530
+ // Delete Confirmation - تأكيد الحذف
9531
+ 'delete.regionTitle': 'حذف المنطقة',
9532
+ 'delete.regionMessage': 'هل أنت متأكد من حذف المنطقة "{{name}}"؟ لا يمكن التراجع عن هذا الإجراء.',
9533
+ 'delete.styleTitle': 'حذف النمط',
9534
+ 'delete.styleMessage': 'هل أنت متأكد من حذف النمط "{{name}}"؟ لا يمكن التراجع عن هذا الإجراء وسيؤثر على المناطق المرتبطة.',
9535
+ // Re-download - إعادة التحميل
9536
+ 'redownload.title': 'إعادة تحميل المنطقة',
9537
+ 'redownload.message': 'إعادة تحميل "{{name}}"؟\n\nسيتم:\n• حذف البلاطات والموارد الحالية\n• إعادة تحميل جميع البيانات بالإعدادات الحالية\n• إصلاح أي موارد تالفة أو قديمة',
9538
+ 'redownload.button': 'إعادة التحميل',
9539
+ // Fix Tiles - إصلاح البلاطات
9540
+ 'fixTiles.noIssuesTitle': 'لم يتم العثور على مشاكل',
9541
+ 'fixTiles.noIssuesMessage': 'لم يتم اكتشاف بلاطات مضغوطة. البلاطات بالتنسيق الصحيح!',
9542
+ 'fixTiles.title': 'إصلاح البلاطات المضغوطة',
9543
+ 'fixTiles.message': 'تم العثور على {{count}} بلاطة مضغوطة قد تسبب أخطاء في العرض.\n\nسيتم:\n1. إزالة جميع البلاطات المضغوطة ({{count}} بلاطة)\n2. ستحتاج إلى إعادة تحميل المناطق للحصول على بلاطات مفكوكة الضغط\n\nهل تريد المتابعة؟',
9544
+ 'fixTiles.button': 'إصلاح البلاطات',
9545
+ 'fixTiles.completeTitle': 'اكتمل التنظيف',
9546
+ 'fixTiles.completeMessage': 'تم إزالة {{count}} بلاطة مضغوطة بنجاح.\n\nيرجى إعادة تحميل مناطقك للحصول على البلاطات المصححة.',
9547
+ // Download Progress - تقدم التحميل
9548
+ 'download.active': 'التحميلات النشطة',
9549
+ 'download.phase.style': 'النمط',
9550
+ 'download.phase.sprites': 'الرموز',
9551
+ 'download.phase.glyphs': 'الخطوط',
9552
+ 'download.phase.tiles': 'البلاطات',
9553
+ // Style Selection - اختيار النمط
9554
+ 'styleSelection.title': 'اختر نمط غير متصل',
9555
+ 'styleSelection.message': 'اختر النمط غير المتصل الذي تريد تحميله:',
9556
+ 'styleSelection.sources': 'مصادر',
9557
+ // Import/Export - استيراد/تصدير
9558
+ 'importExport.title': 'استيراد/تصدير',
9559
+ 'importExport.export': 'تصدير',
9560
+ 'importExport.import': 'استيراد',
9561
+ // Errors - الأخطاء
9562
+ 'error.loadingContent': 'خطأ في تحميل المحتوى',
9563
+ 'error.tryAgain': 'يرجى المحاولة مرة أخرى',
9564
+ 'error.mapNotInitialized': 'الخريطة غير مهيأة. يرجى التأكد من تحميل الخريطة قبل تحميل النمط.',
9565
+ 'error.invalidStyleData': 'بيانات نمط غير صالحة. قد يكون النمط تالفاً.',
9566
+ 'error.styleMissingSources': 'النمط لا يحتوي على مصادر. قد يكون النمط غير مكتمل.',
9567
+ 'error.styleMissingLayers': 'النمط لا يحتوي على طبقات. قد يكون النمط غير مكتمل.',
9568
+ 'error.regionNotFound': 'المنطقة غير موجودة',
9569
+ 'error.downloadFailed': 'فشل التحميل',
9570
+ 'error.deleteFailed': 'فشل الحذف',
9571
+ 'error.loadStyleFailed': 'فشل تحميل النمط',
9572
+ // Warnings - التحذيرات
9573
+ 'warning.compressedTiles': 'تم العثور على {{count}} بلاطة مضغوطة قد تسبب مشاكل في العرض.',
9574
+ 'warning.compressedTilesRecommendation': 'موصى به: استخدم زر "إعادة التحميل" لإصلاح هذه المشكلة.',
9575
+ 'warning.continueAnyway': 'هل تريد الاستمرار في تحميل النمط على أي حال؟',
9576
+ // Drawing Tool - أداة الرسم
9577
+ 'drawing.instructions': 'انقر واسحب لرسم منطقة',
9578
+ 'drawing.cancel': 'اضغط ESC للإلغاء',
9579
+ // Offline Maps Button - زر الخرائط غير المتصلة
9580
+ 'button.offlineMaps': 'خرائط غير متصلة',
9581
+ // Region Details Modal - نافذة تفاصيل المنطقة
9582
+ 'regionDetails.bounds': 'الحدود',
9583
+ 'regionDetails.zoomRange': 'نطاق التكبير',
9584
+ 'regionDetails.created': 'تاريخ الإنشاء',
9585
+ // Import/Export Modal - نافذة الاستيراد/التصدير
9586
+ 'importExport.regionTitle': 'استيراد/تصدير المنطقة',
9587
+ 'importExport.regionInfo': 'معلومات المنطقة',
9588
+ 'importExport.id': 'المعرف',
9589
+ 'importExport.name': 'الاسم',
9590
+ 'importExport.unnamed': 'بدون اسم',
9591
+ 'importExport.zoom': 'التكبير',
9592
+ 'importExport.created': 'تاريخ الإنشاء',
9593
+ 'importExport.exportRegion': 'تصدير المنطقة',
9594
+ 'importExport.exportFormat': 'تنسيق التصدير',
9595
+ 'importExport.formatJson': 'JSON - بيانات كاملة (موصى به)',
9596
+ 'importExport.formatPmtiles': 'PMTiles - بلاطات محسنة للويب',
9597
+ 'importExport.formatMbtiles': 'MBTiles - معيار الصناعة',
9598
+ 'importExport.formatHint': 'اختر التنسيق بناءً على حالة استخدامك',
9599
+ 'importExport.includeComponents': 'تضمين المكونات',
9600
+ 'importExport.styleConfig': 'إعدادات النمط',
9601
+ 'importExport.mapTiles': 'بلاطات الخريطة',
9602
+ 'importExport.spritesIcons': 'الرموز والأيقونات',
9603
+ 'importExport.fontsGlyphs': 'الخطوط والحروف',
9604
+ 'importExport.preparingExport': 'جاري تجهيز التصدير...',
9605
+ 'importExport.exportComplete': 'اكتمل التصدير!',
9606
+ 'importExport.exportFailed': 'فشل التصدير. يرجى المحاولة مرة أخرى.',
9607
+ 'importExport.importRegion': 'استيراد المنطقة',
9608
+ 'importExport.selectFile': 'اختر ملف',
9609
+ 'importExport.fileFormatsHint': 'يدعم تنسيقات JSON و PMTiles و MBTiles',
9610
+ 'importExport.newRegionName': 'اسم المنطقة الجديد (اختياري)',
9611
+ 'importExport.newRegionNamePlaceholder': 'اتركه فارغاً لاستخدام الاسم الأصلي',
9612
+ 'importExport.overwriteIfExists': 'الكتابة فوق المنطقة الموجودة',
9613
+ 'importExport.preparingImport': 'جاري تجهيز الاستيراد...',
9614
+ 'importExport.importComplete': 'اكتمل الاستيراد!',
9615
+ 'importExport.importFailed': 'فشل الاستيراد. يرجى المحاولة مرة أخرى.',
9616
+ 'importExport.formatGuide': 'دليل التنسيقات',
9617
+ 'importExport.jsonDesc': 'بيانات كاملة، قابلة للقراءة، الأفضل للتطوير',
9618
+ 'importExport.pmtilesDesc': 'محسن للويب، خدمة فعالة، متوافق مع السحابة',
9619
+ 'importExport.mbtilesDesc': 'معيار الصناعة، قائم على SQLite، متعدد المنصات',
9620
+ // Active Downloads - التحميلات النشطة
9621
+ 'download.activeCount': 'التحميلات النشطة ({{count}})',
9622
+ // Panel Manager additional strings - سلاسل إضافية للوحة
9623
+ 'panel.downloadedRegions': 'المناطق المحملة',
9624
+ 'panel.noExpiry': 'بدون انتهاء',
9625
+ 'panel.noBounds': 'بدون حدود',
9626
+ 'panel.sources': 'مصادر',
9627
+ // Empty/Error States - حالات الفراغ/الخطأ
9628
+ 'emptyState.title': 'لا توجد عناصر',
9629
+ 'emptyState.message': 'لا توجد عناصر للعرض',
9630
+ 'errorState.message': 'حدث خطأ ما',
9631
+ 'errorState.retry': 'حاول مرة أخرى',
9632
+ // List - القائمة
9633
+ 'list.empty': 'لا توجد عناصر للعرض',
9634
+ 'list.noItemsFound': 'لم يتم العثور على عناصر',
9635
+ // Toast - الإشعارات
9636
+ 'toast.dismiss': 'إغلاق الإشعار',
9637
+ // Control Buttons - أزرار التحكم
9638
+ 'control.offlineMapManager': 'مدير الخرائط غير المتصلة',
9639
+ 'control.cancelSelection': 'إلغاء التحديد',
9640
+ 'control.saveSelectedRegion': 'حفظ المنطقة المحددة',
9641
+ // Alerts/Messages - التنبيهات/الرسائل
9642
+ 'alert.unknownError': 'حدث خطأ غير معروف',
9643
+ 'alert.downloadNotImplemented': 'ميزة التحميل غير مطبقة في هذه النسخة',
9644
+ 'alert.mapNotAvailable': 'الخريطة غير متاحة',
9645
+ 'alert.invalidBounds': 'حدود غير صالحة للمنطقة',
9646
+ 'alert.styleNotFound': 'النمط غير موجود',
9647
+ 'alert.noStylesFound': 'لم يتم العثور على أنماط في IndexedDB',
9648
+ 'alert.failedToApplyStyle': 'فشل تطبيق النمط على الخريطة',
9649
+ 'alert.failedToLoadStyle': 'فشل تحميل النمط',
9650
+ 'alert.failedToRedownload': 'فشل إعادة تحميل المنطقة',
9651
+ 'alert.downloadManagerNotAvailable': 'مدير التحميل غير متاح',
9652
+ // Download Manager - مدير التحميل
9653
+ 'downloadManager.downloadingStyle': 'جاري تحميل النمط',
9654
+ 'downloadManager.downloadingSprites': 'جاري تحميل الرموز',
9655
+ 'downloadManager.downloadingGlyphs': 'جاري تحميل الخطوط',
9656
+ 'downloadManager.downloadingTiles': 'جاري تحميل البلاطات',
9657
+ 'downloadManager.styleExistsNotFound': 'النمط موجود لكن لم يتم العثور عليه',
9658
+ 'downloadManager.styleNotFoundForResources': 'لم يتم العثور على النمط لتحميل الموارد',
9659
+ 'downloadManager.styleNotFoundForTiles': 'لم يتم العثور على النمط لتحميل البلاطات',
9660
+ 'downloadManager.styleNoSources': 'النمط لا يحتوي على مصادر لتحميل البلاطات',
9661
+ // Form Validation - التحقق من النموذج
9662
+ 'validation.required': 'هذا الحقل مطلوب',
9663
+ 'validation.invalidFormat': 'تنسيق غير صالح',
9664
+ 'validation.mustBeNumber': 'يجب أن يكون رقماً',
9665
+ 'validation.mustBeWholeNumber': 'يجب أن يكون رقماً صحيحاً',
9666
+ 'validation.invalidUrl': 'تنسيق رابط غير صالح',
9667
+ };
9668
+
9669
+ /**
9670
+ * Internationalization (i18n) module for map-gl-offline
9671
+ * Uses i18next for multi-language support
9672
+ *
9673
+ * Currently supports: English (en), Arabic (ar)
9674
+ */
9675
+ // RTL languages
9676
+ const rtlLanguages = ['ar'];
9677
+ // Initialize i18next
9678
+ instance.init({
9679
+ lng: localStorage.getItem('offline-manager-language') || 'en',
9680
+ fallbackLng: 'en',
9681
+ debug: false,
9682
+ interpolation: {
9683
+ escapeValue: false, // Not needed for non-HTML contexts
9684
+ prefix: '{{',
9685
+ suffix: '}}',
9686
+ },
9687
+ resources: {
9688
+ en: { translation: en },
9689
+ ar: { translation: ar },
9690
+ },
9691
+ });
9692
+ /**
9693
+ * I18n Manager wrapper for i18next with additional functionality
9694
+ */
9695
+ class I18nManager {
9696
+ listeners = new Set();
6940
9697
  /**
6941
- * Get the effective theme mode (light or dark)
9698
+ * Get the current language
6942
9699
  */
6943
- getEffectiveMode() {
6944
- return this.currentTheme.mode;
9700
+ getLanguage() {
9701
+ return instance.language;
6945
9702
  }
6946
9703
  /**
6947
- * Get the user's preference (light, dark, or system)
9704
+ * Set the current language
6948
9705
  */
6949
- getPreference() {
6950
- return this.currentTheme.preference;
6951
- }
6952
- applyThemeMode(mode, preference) {
6953
- this.currentTheme = { mode, preference };
6954
- // Apply theme to document for Tailwind dark mode
6955
- if (mode === 'dark') {
6956
- document.documentElement.classList.add('dark');
6957
- }
6958
- else {
6959
- document.documentElement.classList.remove('dark');
6960
- }
9706
+ setLanguage(lang) {
9707
+ instance.changeLanguage(lang);
9708
+ localStorage.setItem('offline-manager-language', lang);
9709
+ // Update document direction for RTL languages
9710
+ this.updateDocumentDirection();
9711
+ // Notify listeners
6961
9712
  this.notifyListeners();
6962
9713
  }
6963
9714
  /**
6964
- * Set the theme preference (light, dark, or system)
9715
+ * Check if current language is RTL
6965
9716
  */
6966
- setThemePreference(preference) {
6967
- localStorage.setItem('offline-manager-theme', preference);
6968
- if (preference === 'system') {
6969
- const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
6970
- this.applyThemeMode(systemPrefersDark ? 'dark' : 'light', 'system');
6971
- }
6972
- else {
6973
- this.applyThemeMode(preference, preference);
6974
- }
9717
+ isRTL() {
9718
+ return rtlLanguages.includes(this.getLanguage());
6975
9719
  }
6976
9720
  /**
6977
- * Set the theme mode directly (for backwards compatibility)
6978
- * @deprecated Use setThemePreference instead
9721
+ * Update document direction based on current language
6979
9722
  */
6980
- setTheme(mode) {
6981
- this.setThemePreference(mode);
9723
+ updateDocumentDirection() {
9724
+ const offlineManagerElements = document.querySelectorAll('.offline-manager-control');
9725
+ offlineManagerElements.forEach(el => {
9726
+ if (this.isRTL()) {
9727
+ el.setAttribute('dir', 'rtl');
9728
+ el.classList.add('rtl');
9729
+ }
9730
+ else {
9731
+ el.setAttribute('dir', 'ltr');
9732
+ el.classList.remove('rtl');
9733
+ }
9734
+ });
6982
9735
  }
6983
9736
  /**
6984
- * Cycle through themes: light -> dark -> system -> light
9737
+ * Get a translated string using i18next
6985
9738
  */
6986
- cycleTheme() {
6987
- const preference = this.currentTheme.preference;
6988
- if (preference === 'light') {
6989
- this.setThemePreference('dark');
6990
- }
6991
- else if (preference === 'dark') {
6992
- this.setThemePreference('system');
6993
- }
6994
- else {
6995
- this.setThemePreference('light');
6996
- }
9739
+ t(key, replacements) {
9740
+ return instance.t(key, replacements);
6997
9741
  }
6998
9742
  /**
6999
- * Toggle between light and dark (legacy behavior)
7000
- * @deprecated Use cycleTheme for light/dark/system cycling
9743
+ * Get all available languages
7001
9744
  */
7002
- toggleTheme() {
7003
- this.cycleTheme();
9745
+ getAvailableLanguages() {
9746
+ return [
9747
+ { code: 'en', name: 'English', nativeName: 'English' },
9748
+ { code: 'ar', name: 'Arabic', nativeName: 'العربية' },
9749
+ ];
7004
9750
  }
7005
- subscribe(listener) {
7006
- this.listeners.push(listener);
7007
- return () => {
7008
- const index = this.listeners.indexOf(listener);
7009
- if (index > -1) {
7010
- this.listeners.splice(index, 1);
7011
- }
7012
- };
9751
+ /**
9752
+ * Subscribe to language changes
9753
+ */
9754
+ subscribe(callback) {
9755
+ this.listeners.add(callback);
9756
+ return () => this.listeners.delete(callback);
7013
9757
  }
9758
+ /**
9759
+ * Notify all listeners of language change
9760
+ */
7014
9761
  notifyListeners() {
7015
- this.listeners.forEach(listener => listener(this.currentTheme));
9762
+ this.listeners.forEach(callback => callback());
9763
+ }
9764
+ /**
9765
+ * Get the i18next instance for advanced usage
9766
+ */
9767
+ getInstance() {
9768
+ return instance;
7016
9769
  }
7017
9770
  }
7018
- // Export singleton instance
7019
- const themeManager = new ThemeManager();
9771
+ // Singleton instance
9772
+ const i18n = new I18nManager();
9773
+ // Convenience function for translations
9774
+ const t = (key, replacements) => {
9775
+ return i18n.t(key, replacements);
9776
+ };
7020
9777
 
7021
9778
  /**
7022
9779
  * Base Component Class
@@ -7368,7 +10125,7 @@ class ButtonManager {
7368
10125
  const buttonConfig = {
7369
10126
  className: 'maplibregl-ctrl-icon relative',
7370
10127
  icon: icons.cloud({ size: 20, color: 'black' }),
7371
- title: 'Offline Map Manager',
10128
+ title: t('control.offlineMapManager'),
7372
10129
  showProgressBadge: true,
7373
10130
  onClick: this.options.onTogglePanel,
7374
10131
  };
@@ -7476,13 +10233,13 @@ class Modal extends BaseComponent {
7476
10233
  actions.className = 'flex items-center gap-2';
7477
10234
  // Theme toggle button
7478
10235
  if (this.config.showThemeToggle && this.config.onThemeToggle) {
7479
- const themeButton = this.createIconButton(`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>`, 'Toggle theme');
10236
+ const themeButton = this.createIconButton(`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>`, t('theme.toggle'));
7480
10237
  themeButton.addEventListener('click', this.config.onThemeToggle);
7481
10238
  actions.appendChild(themeButton);
7482
10239
  }
7483
10240
  // Close button
7484
10241
  if (this.config.closable && this.config.onClose) {
7485
- const closeButton = this.createIconButton(`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`, 'Close');
10242
+ const closeButton = this.createIconButton(`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`, t('app.close'));
7486
10243
  closeButton.addEventListener('click', this.config.onClose);
7487
10244
  actions.appendChild(closeButton);
7488
10245
  }
@@ -7598,7 +10355,7 @@ class RegionDetailsModal {
7598
10355
  show() {
7599
10356
  const { region } = this.options;
7600
10357
  const modalConfig = {
7601
- title: 'Region Details',
10358
+ title: t('regionDetails.title'),
7602
10359
  size: 'sm',
7603
10360
  closable: true,
7604
10361
  showThemeToggle: false,
@@ -7621,39 +10378,42 @@ class RegionDetailsModal {
7621
10378
  createContent(region) {
7622
10379
  const content = document.createElement('div');
7623
10380
  content.className = 'flex flex-col gap-5';
10381
+ if (i18n.isRTL()) {
10382
+ content.setAttribute('dir', 'rtl');
10383
+ }
7624
10384
  content.innerHTML = `
7625
10385
  <div class="glass-input p-4 rounded-xl border-0 bg-gray-50/50 dark:bg-gray-800/50">
7626
10386
  <h3 class="m-0 text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-gray-900 to-gray-600 dark:from-white dark:to-gray-300">
7627
10387
  ${region.name}
7628
10388
  </h3>
7629
10389
  </div>
7630
-
10390
+
7631
10391
  <div class="grid grid-cols-2 gap-4">
7632
10392
  <div class="glass-input p-4 rounded-xl border-0 bg-gray-50/30 dark:bg-gray-800/30">
7633
10393
  <label class="block mb-1.5 text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
7634
- Bounds
10394
+ ${t('regionDetails.bounds')}
7635
10395
  </label>
7636
10396
  <div class="text-sm font-mono text-gray-700 dark:text-gray-300 leading-relaxed">
7637
10397
  ${region.bounds[0][1].toFixed(4)}, ${region.bounds[0][0].toFixed(4)}<br>
7638
10398
  ${region.bounds[1][1].toFixed(4)}, ${region.bounds[1][0].toFixed(4)}
7639
10399
  </div>
7640
10400
  </div>
7641
-
10401
+
7642
10402
  <div class="glass-input p-4 rounded-xl border-0 bg-gray-50/30 dark:bg-gray-800/30">
7643
10403
  <label class="block mb-1.5 text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
7644
- Zoom Range
10404
+ ${t('regionDetails.zoomRange')}
7645
10405
  </label>
7646
10406
  <div class="text-lg font-semibold text-gray-900 dark:text-white">
7647
10407
  ${region.minZoom} - ${region.maxZoom}
7648
10408
  </div>
7649
10409
  </div>
7650
10410
  </div>
7651
-
10411
+
7652
10412
  ${region.created
7653
10413
  ? `
7654
10414
  <div class="glass-input p-4 rounded-xl border-0 bg-gray-50/30 dark:bg-gray-800/30">
7655
10415
  <label class="block mb-1.5 text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">
7656
- Created
10416
+ ${t('regionDetails.created')}
7657
10417
  </label>
7658
10418
  <div class="text-sm font-medium text-gray-700 dark:text-gray-300">
7659
10419
  ${formatDate(region.created)}
@@ -7670,16 +10430,19 @@ class RegionDetailsModal {
7670
10430
  createFooter() {
7671
10431
  const footer = document.createElement('div');
7672
10432
  footer.className = 'flex gap-2 justify-end';
10433
+ if (i18n.isRTL()) {
10434
+ footer.setAttribute('dir', 'rtl');
10435
+ }
7673
10436
  // Focus button
7674
10437
  const focusButton = new Button({
7675
- text: 'Focus on Map',
10438
+ text: t('regionDetails.focusOnMap'),
7676
10439
  variant: 'secondary',
7677
10440
  icon: icons.focus({ size: 16, color: 'currentColor' }),
7678
10441
  onClick: () => this.focusRegion(),
7679
10442
  });
7680
10443
  // Close button
7681
10444
  const closeButton = new Button({
7682
- text: 'Close',
10445
+ text: t('app.close'),
7683
10446
  variant: 'primary',
7684
10447
  onClick: () => this.close(),
7685
10448
  });
@@ -7806,7 +10569,7 @@ class ImportExportModal {
7806
10569
  }
7807
10570
  show() {
7808
10571
  const modalConfig = {
7809
- title: 'Import/Export Region',
10572
+ title: t('importExport.regionTitle'),
7810
10573
  subtitle: this.options.region.name || this.options.region.id,
7811
10574
  size: 'md',
7812
10575
  closable: true,
@@ -7830,6 +10593,9 @@ class ImportExportModal {
7830
10593
  createContent() {
7831
10594
  const content = document.createElement('div');
7832
10595
  content.className = 'flex flex-col gap-6';
10596
+ if (i18n.isRTL()) {
10597
+ content.setAttribute('dir', 'rtl');
10598
+ }
7833
10599
  // Region Info Card
7834
10600
  const infoCard = this.createRegionInfoCard();
7835
10601
  content.appendChild(infoCard);
@@ -7854,23 +10620,23 @@ class ImportExportModal {
7854
10620
  card.innerHTML = `
7855
10621
  <h4 class="text-sm font-bold uppercase tracking-wider text-gray-900 dark:text-white mb-4 flex items-center gap-2">
7856
10622
  ${icons.mapPin({ size: 16, color: 'currentColor' })}
7857
- Region Information
10623
+ ${t('importExport.regionInfo')}
7858
10624
  </h4>
7859
10625
  <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
7860
10626
  <div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
7861
- <span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">ID</span>
10627
+ <span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.id')}</span>
7862
10628
  <div class="text-gray-900 dark:text-white font-mono text-xs break-all mt-1">${this.options.region.id}</div>
7863
10629
  </div>
7864
10630
  <div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
7865
- <span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Name</span>
7866
- <div class="text-gray-900 dark:text-white font-medium mt-1">${this.options.region.name || 'Unnamed'}</div>
10631
+ <span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.name')}</span>
10632
+ <div class="text-gray-900 dark:text-white font-medium mt-1">${this.options.region.name || t('importExport.unnamed')}</div>
7867
10633
  </div>
7868
10634
  <div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
7869
- <span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Zoom</span>
10635
+ <span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.zoom')}</span>
7870
10636
  <div class="text-gray-900 dark:text-white font-medium mt-1">Z${this.options.region.minZoom}-${this.options.region.maxZoom}</div>
7871
10637
  </div>
7872
10638
  <div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
7873
- <span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Created</span>
10639
+ <span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.created')}</span>
7874
10640
  <div class="text-gray-900 dark:text-white font-medium mt-1">${new Date(this.options.region.created).toLocaleDateString()}</div>
7875
10641
  </div>
7876
10642
  </div>
@@ -7883,7 +10649,7 @@ class ImportExportModal {
7883
10649
  'glass-input p-6 rounded-xl border-0 bg-white/40 dark:bg-gray-800/40 relative overflow-hidden group hover:scale-[1.01] transition-transform duration-300';
7884
10650
  // Gradient accent
7885
10651
  const accent = document.createElement('div');
7886
- accent.className = 'absolute top-0 left-0 w-1 h-full bg-blue-500 opacity-50';
10652
+ accent.className = `absolute top-0 ${i18n.isRTL() ? 'right-0' : 'left-0'} w-1 h-full bg-blue-500 opacity-50`;
7887
10653
  section.appendChild(accent);
7888
10654
  const header = document.createElement('h3');
7889
10655
  header.className =
@@ -7892,7 +10658,7 @@ class ImportExportModal {
7892
10658
  <div class="p-2 bg-blue-500/10 rounded-lg text-blue-600 dark:text-blue-400">
7893
10659
  ${icons.upload({ size: 20, color: 'currentColor' })}
7894
10660
  </div>
7895
- Export Region
10661
+ ${t('importExport.exportRegion')}
7896
10662
  `;
7897
10663
  section.appendChild(header);
7898
10664
  const formContainer = document.createElement('div');
@@ -7901,18 +10667,18 @@ class ImportExportModal {
7901
10667
  const formatGroup = document.createElement('div');
7902
10668
  const formatLabel = document.createElement('label');
7903
10669
  formatLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
7904
- formatLabel.textContent = 'Export Format';
10670
+ formatLabel.textContent = t('importExport.exportFormat');
7905
10671
  this.exportFormatSelect = document.createElement('select');
7906
10672
  this.exportFormatSelect.className =
7907
10673
  'w-full px-4 py-3 rounded-xl text-sm glass-input text-gray-900 dark:text-white bg-white/50 dark:bg-gray-700/50 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all';
7908
10674
  this.exportFormatSelect.innerHTML = `
7909
- <option value="json">JSON - Complete data (recommended)</option>
7910
- <option value="pmtiles">PMTiles - Web optimized tiles</option>
7911
- <option value="mbtiles">MBTiles - Industry standard</option>
10675
+ <option value="json">${t('importExport.formatJson')}</option>
10676
+ <option value="pmtiles">${t('importExport.formatPmtiles')}</option>
10677
+ <option value="mbtiles">${t('importExport.formatMbtiles')}</option>
7912
10678
  `;
7913
10679
  const formatHint = document.createElement('p');
7914
10680
  formatHint.className = 'mt-2 text-xs text-gray-500 dark:text-gray-400 ml-1';
7915
- formatHint.textContent = 'Choose format based on your use case';
10681
+ formatHint.textContent = t('importExport.formatHint');
7916
10682
  formatGroup.appendChild(formatLabel);
7917
10683
  formatGroup.appendChild(this.exportFormatSelect);
7918
10684
  formatGroup.appendChild(formatHint);
@@ -7921,7 +10687,7 @@ class ImportExportModal {
7921
10687
  const optionsGroup = document.createElement('div');
7922
10688
  const optionsLabel = document.createElement('label');
7923
10689
  optionsLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3';
7924
- optionsLabel.textContent = 'Include Components';
10690
+ optionsLabel.textContent = t('importExport.includeComponents');
7925
10691
  const checkboxContainer = document.createElement('div');
7926
10692
  checkboxContainer.className = 'grid grid-cols-1 sm:grid-cols-2 gap-3';
7927
10693
  const createCheckbox = (text, checked = true) => {
@@ -7940,16 +10706,16 @@ class ImportExportModal {
7940
10706
  label.appendChild(span);
7941
10707
  return { label, input };
7942
10708
  };
7943
- const styleCheck = createCheckbox('Style Configuration');
10709
+ const styleCheck = createCheckbox(t('importExport.styleConfig'));
7944
10710
  this.includeStyleCheckbox = styleCheck.input;
7945
10711
  checkboxContainer.appendChild(styleCheck.label);
7946
- const tilesCheck = createCheckbox('Map Tiles');
10712
+ const tilesCheck = createCheckbox(t('importExport.mapTiles'));
7947
10713
  this.includeTilesCheckbox = tilesCheck.input;
7948
10714
  checkboxContainer.appendChild(tilesCheck.label);
7949
- const spritesCheck = createCheckbox('Sprites & Icons');
10715
+ const spritesCheck = createCheckbox(t('importExport.spritesIcons'));
7950
10716
  this.includeSpritesCheckbox = spritesCheck.input;
7951
10717
  checkboxContainer.appendChild(spritesCheck.label);
7952
- const fontsCheck = createCheckbox('Fonts & Glyphs');
10718
+ const fontsCheck = createCheckbox(t('importExport.fontsGlyphs'));
7953
10719
  this.includeFontsCheckbox = fontsCheck.input;
7954
10720
  checkboxContainer.appendChild(fontsCheck.label);
7955
10721
  optionsGroup.appendChild(optionsLabel);
@@ -7967,13 +10733,13 @@ class ImportExportModal {
7967
10733
  progressBarContainer.appendChild(this.exportProgressBar);
7968
10734
  this.exportProgressText = document.createElement('p');
7969
10735
  this.exportProgressText.className = 'text-sm text-gray-600 dark:text-gray-400';
7970
- this.exportProgressText.textContent = 'Preparing export...';
10736
+ this.exportProgressText.textContent = t('importExport.preparingExport');
7971
10737
  progressContainer.appendChild(progressBarContainer);
7972
10738
  progressContainer.appendChild(this.exportProgressText);
7973
10739
  formContainer.appendChild(progressContainer);
7974
10740
  // Export Button
7975
10741
  const exportButton = new Button({
7976
- text: 'Export Region',
10742
+ text: t('importExport.exportRegion'),
7977
10743
  variant: 'primary',
7978
10744
  icon: icons.download({ size: 16, color: 'white' }),
7979
10745
  className: 'w-full py-2.5 text-base shadow-lg shadow-blue-500/20', // Premium button styles
@@ -7990,7 +10756,7 @@ class ImportExportModal {
7990
10756
  'glass-input p-6 rounded-xl border-0 bg-white/40 dark:bg-gray-800/40 relative overflow-hidden group hover:scale-[1.01] transition-transform duration-300';
7991
10757
  // Gradient accent
7992
10758
  const accent = document.createElement('div');
7993
- accent.className = 'absolute top-0 left-0 w-1 h-full bg-green-500 opacity-50';
10759
+ accent.className = `absolute top-0 ${i18n.isRTL() ? 'right-0' : 'left-0'} w-1 h-full bg-green-500 opacity-50`;
7994
10760
  section.appendChild(accent);
7995
10761
  const header = document.createElement('h3');
7996
10762
  header.className =
@@ -7999,7 +10765,7 @@ class ImportExportModal {
7999
10765
  <div class="p-2 bg-green-500/10 rounded-lg text-green-600 dark:text-green-400">
8000
10766
  ${icons.upload({ size: 20, color: 'currentColor' })}
8001
10767
  </div>
8002
- Import Region
10768
+ ${t('importExport.importRegion')}
8003
10769
  `;
8004
10770
  section.appendChild(header);
8005
10771
  const formContainer = document.createElement('div');
@@ -8008,7 +10774,7 @@ class ImportExportModal {
8008
10774
  const fileGroup = document.createElement('div');
8009
10775
  const fileLabel = document.createElement('label');
8010
10776
  fileLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
8011
- fileLabel.textContent = 'Select File';
10777
+ fileLabel.textContent = t('importExport.selectFile');
8012
10778
  this.importFileInput = document.createElement('input');
8013
10779
  this.importFileInput.type = 'file';
8014
10780
  this.importFileInput.accept = '.json,.pmtiles,.mbtiles';
@@ -8016,7 +10782,7 @@ class ImportExportModal {
8016
10782
  'w-full text-sm text-gray-500 file:mr-4 file:py-2.5 file:px-4 file:rounded-xl file:border-0 file:text-sm file:font-semibold file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100 dark:file:bg-primary-900/20 dark:file:text-primary-400 glass-input transition-all cursor-pointer';
8017
10783
  const fileHint = document.createElement('p');
8018
10784
  fileHint.className = 'mt-2 text-xs text-gray-500 dark:text-gray-400 ml-1';
8019
- fileHint.textContent = 'Supports JSON, PMTiles, and MBTiles formats';
10785
+ fileHint.textContent = t('importExport.fileFormatsHint');
8020
10786
  fileGroup.appendChild(fileLabel);
8021
10787
  fileGroup.appendChild(this.importFileInput);
8022
10788
  fileGroup.appendChild(fileHint);
@@ -8025,10 +10791,10 @@ class ImportExportModal {
8025
10791
  const nameGroup = document.createElement('div');
8026
10792
  const nameLabel = document.createElement('label');
8027
10793
  nameLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
8028
- nameLabel.textContent = 'New Region Name (Optional)';
10794
+ nameLabel.textContent = t('importExport.newRegionName');
8029
10795
  this.importNameInput = document.createElement('input');
8030
10796
  this.importNameInput.type = 'text';
8031
- this.importNameInput.placeholder = 'Leave empty to use original name';
10797
+ this.importNameInput.placeholder = t('importExport.newRegionNamePlaceholder');
8032
10798
  this.importNameInput.className =
8033
10799
  'w-full px-4 py-3 rounded-xl text-sm glass-input text-gray-900 dark:text-white bg-white/50 dark:bg-gray-700/50 focus:outline-none focus:ring-2 focus:ring-green-500/50 transition-all';
8034
10800
  nameGroup.appendChild(nameLabel);
@@ -8044,7 +10810,7 @@ class ImportExportModal {
8044
10810
  'w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-green-600 focus:ring-green-500 focus:ring-2 dark:bg-gray-700';
8045
10811
  const overwriteSpan = document.createElement('span');
8046
10812
  overwriteSpan.className = 'text-sm text-gray-700 dark:text-gray-300 font-medium';
8047
- overwriteSpan.textContent = 'Overwrite if region exists';
10813
+ overwriteSpan.textContent = t('importExport.overwriteIfExists');
8048
10814
  overwriteLabel.appendChild(this.importOverwriteCheckbox);
8049
10815
  overwriteLabel.appendChild(overwriteSpan);
8050
10816
  formContainer.appendChild(overwriteLabel);
@@ -8060,13 +10826,13 @@ class ImportExportModal {
8060
10826
  progressBarContainer.appendChild(this.importProgressBar);
8061
10827
  this.importProgressText = document.createElement('p');
8062
10828
  this.importProgressText.className = 'text-sm text-gray-600 dark:text-gray-400';
8063
- this.importProgressText.textContent = 'Preparing import...';
10829
+ this.importProgressText.textContent = t('importExport.preparingImport');
8064
10830
  progressContainer.appendChild(progressBarContainer);
8065
10831
  progressContainer.appendChild(this.importProgressText);
8066
10832
  formContainer.appendChild(progressContainer);
8067
10833
  // Import Button
8068
10834
  const importButton = new Button({
8069
- text: 'Import Region',
10835
+ text: t('importExport.importRegion'),
8070
10836
  variant: 'success', // Assuming 'success' variant exists in Button component, if not might need style adjustment. Assuming it works based on previous code.
8071
10837
  icon: icons.upload({ size: 16, color: 'white' }),
8072
10838
  className: 'w-full py-2.5 text-base shadow-lg shadow-green-500/20',
@@ -8084,20 +10850,20 @@ class ImportExportModal {
8084
10850
  guide.innerHTML = `
8085
10851
  <h4 class="text-sm font-bold uppercase tracking-wider text-blue-900 dark:text-blue-300 mb-3 flex items-center gap-2">
8086
10852
  ${icons.infoCircle({ size: 16, color: 'currentColor' })}
8087
- Format Guide
10853
+ ${t('importExport.formatGuide')}
8088
10854
  </h4>
8089
10855
  <div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
8090
10856
  <div class="p-3 rounded-lg bg-white/50 dark:bg-black/20">
8091
10857
  <div class="font-bold text-base text-blue-800 dark:text-blue-300 mb-1">JSON</div>
8092
- <div class="text-blue-700 dark:text-blue-400 leading-relaxed">Complete data, human-readable, best for development</div>
10858
+ <div class="text-blue-700 dark:text-blue-400 leading-relaxed">${t('importExport.jsonDesc')}</div>
8093
10859
  </div>
8094
10860
  <div class="p-3 rounded-lg bg-white/50 dark:bg-black/20">
8095
10861
  <div class="font-bold text-base text-blue-800 dark:text-blue-300 mb-1">PMTiles</div>
8096
- <div class="text-blue-700 dark:text-blue-400 leading-relaxed">Web-optimized, efficient serving, cloud-friendly</div>
10862
+ <div class="text-blue-700 dark:text-blue-400 leading-relaxed">${t('importExport.pmtilesDesc')}</div>
8097
10863
  </div>
8098
10864
  <div class="p-3 rounded-lg bg-white/50 dark:bg-black/20">
8099
10865
  <div class="font-bold text-base text-blue-800 dark:text-blue-300 mb-1">MBTiles</div>
8100
- <div class="text-blue-700 dark:text-blue-400 leading-relaxed">Industry standard, SQLite-based, cross-platform</div>
10866
+ <div class="text-blue-700 dark:text-blue-400 leading-relaxed">${t('importExport.mbtilesDesc')}</div>
8101
10867
  </div>
8102
10868
  </div>
8103
10869
  `;
@@ -8144,7 +10910,7 @@ class ImportExportModal {
8144
10910
  this.exportProgressBar.style.width = '100%';
8145
10911
  }
8146
10912
  if (this.exportProgressText) {
8147
- this.exportProgressText.textContent = 'Export complete!';
10913
+ this.exportProgressText.textContent = t('importExport.exportComplete');
8148
10914
  }
8149
10915
  this.options.onExport?.(result);
8150
10916
  // Hide modal after short delay
@@ -8153,7 +10919,7 @@ class ImportExportModal {
8153
10919
  catch (error) {
8154
10920
  modalLogger.error('Export error:', error instanceof Error ? error.message : String(error));
8155
10921
  if (this.exportProgressText) {
8156
- this.exportProgressText.textContent = 'Export failed. Please try again.';
10922
+ this.exportProgressText.textContent = t('importExport.exportFailed');
8157
10923
  this.exportProgressText.classList.add('text-red-600', 'dark:text-red-400');
8158
10924
  }
8159
10925
  }
@@ -8193,7 +10959,7 @@ class ImportExportModal {
8193
10959
  this.importProgressBar.style.width = '100%';
8194
10960
  }
8195
10961
  if (this.importProgressText) {
8196
- this.importProgressText.textContent = 'Import complete!';
10962
+ this.importProgressText.textContent = t('importExport.importComplete');
8197
10963
  }
8198
10964
  this.options.onImport?.(result);
8199
10965
  // Hide modal after short delay
@@ -8202,7 +10968,7 @@ class ImportExportModal {
8202
10968
  catch (error) {
8203
10969
  modalLogger.error('Import error:', error instanceof Error ? error.message : String(error));
8204
10970
  if (this.importProgressText) {
8205
- this.importProgressText.textContent = 'Import failed. Please try again.';
10971
+ this.importProgressText.textContent = t('importExport.importFailed');
8206
10972
  this.importProgressText.classList.add('text-red-600', 'dark:text-red-400');
8207
10973
  }
8208
10974
  }
@@ -8215,8 +10981,11 @@ class ImportExportModal {
8215
10981
  createFooter() {
8216
10982
  const footer = document.createElement('div');
8217
10983
  footer.className = 'flex gap-3 justify-end';
10984
+ if (i18n.isRTL()) {
10985
+ footer.setAttribute('dir', 'rtl');
10986
+ }
8218
10987
  const closeButton = new Button({
8219
- text: 'Close',
10988
+ text: t('app.close'),
8220
10989
  variant: 'secondary',
8221
10990
  onClick: () => this.hide(),
8222
10991
  });
@@ -8238,7 +11007,7 @@ class List extends BaseComponent {
8238
11007
  constructor(config = {}) {
8239
11008
  super(config);
8240
11009
  this.config = {
8241
- emptyText: 'No items to display',
11010
+ emptyText: t('list.empty'),
8242
11011
  ...config,
8243
11012
  };
8244
11013
  this.createListStructure();
@@ -8270,7 +11039,7 @@ class List extends BaseComponent {
8270
11039
  renderEmptyState() {
8271
11040
  const emptyElement = document.createElement('div');
8272
11041
  emptyElement.className = 'text-center py-8 text-gray-500 dark:text-gray-400';
8273
- emptyElement.textContent = this.config.emptyText || 'No items found';
11042
+ emptyElement.textContent = this.config.emptyText || t('list.noItemsFound');
8274
11043
  this.listContainer?.appendChild(emptyElement);
8275
11044
  }
8276
11045
  createItemElement(item) {
@@ -8416,6 +11185,102 @@ class List extends BaseComponent {
8416
11185
  }
8417
11186
  }
8418
11187
 
11188
+ /**
11189
+ * Language Selector Component
11190
+ * Provides a dropdown to switch between available languages
11191
+ */
11192
+ class LanguageSelector extends BaseComponent {
11193
+ options;
11194
+ dropdown;
11195
+ isOpen = false;
11196
+ constructor(options = {}) {
11197
+ super({});
11198
+ this.options = options;
11199
+ this.render();
11200
+ // Subscribe to language changes
11201
+ i18n.subscribe(() => this.render());
11202
+ }
11203
+ createElement() {
11204
+ const container = document.createElement('div');
11205
+ container.className = 'relative';
11206
+ return container;
11207
+ }
11208
+ render() {
11209
+ const currentLang = i18n.getLanguage();
11210
+ const languages = i18n.getAvailableLanguages();
11211
+ const currentLangInfo = languages.find(l => l.code === currentLang);
11212
+ this.element.innerHTML = '';
11213
+ // Create button
11214
+ const button = document.createElement('button');
11215
+ button.className =
11216
+ 'p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full flex items-center gap-1 transition-colors';
11217
+ button.title = i18n.t('language.select');
11218
+ button.innerHTML = `
11219
+ ${icons.language({ size: 16, color: 'currentColor' })}
11220
+ <span class="text-xs font-medium">${currentLangInfo?.code.toUpperCase() || 'EN'}</span>
11221
+ `;
11222
+ button.addEventListener('click', e => {
11223
+ e.stopPropagation();
11224
+ this.toggleDropdown();
11225
+ });
11226
+ this.element.appendChild(button);
11227
+ // Create dropdown
11228
+ this.dropdown = document.createElement('div');
11229
+ this.dropdown.className = `
11230
+ absolute top-full ${i18n.isRTL() ? 'left-0' : 'right-0'} mt-1
11231
+ bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700
11232
+ rounded-lg shadow-lg overflow-hidden z-50
11233
+ ${this.isOpen ? 'block' : 'hidden'}
11234
+ `;
11235
+ languages.forEach(lang => {
11236
+ const item = document.createElement('button');
11237
+ item.className = `
11238
+ w-full px-4 py-2 text-left text-sm flex items-center justify-between gap-4
11239
+ ${lang.code === currentLang ? 'bg-primary-50 dark:bg-primary-900/20 text-primary-700 dark:text-primary-300' : 'text-gray-700 dark:text-gray-300'}
11240
+ hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors
11241
+ `;
11242
+ item.innerHTML = `
11243
+ <span class="font-medium">${lang.nativeName}</span>
11244
+ <span class="text-xs text-gray-500 dark:text-gray-400">${lang.name}</span>
11245
+ `;
11246
+ item.addEventListener('click', e => {
11247
+ e.stopPropagation();
11248
+ this.selectLanguage(lang.code);
11249
+ });
11250
+ this.dropdown.appendChild(item);
11251
+ });
11252
+ this.element.appendChild(this.dropdown);
11253
+ // Close dropdown on outside click
11254
+ document.addEventListener('click', this.handleOutsideClick);
11255
+ }
11256
+ handleOutsideClick = () => {
11257
+ if (this.isOpen) {
11258
+ this.closeDropdown();
11259
+ }
11260
+ };
11261
+ toggleDropdown() {
11262
+ this.isOpen = !this.isOpen;
11263
+ if (this.dropdown) {
11264
+ this.dropdown.classList.toggle('hidden', !this.isOpen);
11265
+ }
11266
+ }
11267
+ closeDropdown() {
11268
+ this.isOpen = false;
11269
+ if (this.dropdown) {
11270
+ this.dropdown.classList.add('hidden');
11271
+ }
11272
+ }
11273
+ selectLanguage(code) {
11274
+ i18n.setLanguage(code);
11275
+ this.closeDropdown();
11276
+ this.options.onChange?.(code);
11277
+ }
11278
+ destroy() {
11279
+ document.removeEventListener('click', this.handleOutsideClick);
11280
+ super.destroy();
11281
+ }
11282
+ }
11283
+
8419
11284
  /**
8420
11285
  * Panel Renderer Component
8421
11286
  * Handles rendering of the main offline manager panel
@@ -8433,9 +11298,12 @@ class PanelRenderer extends BaseComponent {
8433
11298
  actionButtonsContainer;
8434
11299
  regionsList;
8435
11300
  downloadProgressContainer;
11301
+ languageSelector;
8436
11302
  // Debounce mechanism
8437
11303
  refreshTimeout;
8438
11304
  isRefreshing = false;
11305
+ // Language change unsubscribe function
11306
+ unsubscribeLanguage;
8439
11307
  constructor(options) {
8440
11308
  super({});
8441
11309
  this.offlineManager = options.offlineManager;
@@ -8443,6 +11311,10 @@ class PanelRenderer extends BaseComponent {
8443
11311
  this.modalManager = options.modalManager;
8444
11312
  this.options = options;
8445
11313
  this.map = options.map;
11314
+ // Subscribe to language changes
11315
+ this.unsubscribeLanguage = i18n.subscribe(() => {
11316
+ this.refresh();
11317
+ });
8446
11318
  }
8447
11319
  createElement() {
8448
11320
  const container = document.createElement('div');
@@ -8487,31 +11359,42 @@ class PanelRenderer extends BaseComponent {
8487
11359
  this.headerContainer = document.createElement('div');
8488
11360
  this.headerContainer.className =
8489
11361
  'flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700';
11362
+ // Apply RTL direction if needed
11363
+ if (i18n.isRTL()) {
11364
+ this.headerContainer.setAttribute('dir', 'rtl');
11365
+ }
8490
11366
  // Title section
8491
11367
  const titleSection = document.createElement('div');
8492
11368
  titleSection.innerHTML = `
8493
- <h2 class="text-xl font-semibold text-gray-900 dark:text-white">Offline Manager</h2>
11369
+ <h2 class="text-xl font-semibold text-gray-900 dark:text-white">${t('header.title')}</h2>
8494
11370
  <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
8495
- ${regions.length} regions • ${formatBytes(analytics.totalStorageSize)} total
11371
+ ${t('header.subtitle', { count: regions.length, size: formatBytes(analytics.totalStorageSize) })}
8496
11372
  </p>
8497
11373
  `;
8498
11374
  // Actions section
8499
11375
  const actionsSection = document.createElement('div');
8500
11376
  actionsSection.className = 'flex items-center gap-2';
11377
+ // Language selector
11378
+ this.languageSelector = new LanguageSelector({
11379
+ onChange: () => {
11380
+ // Language change is handled by subscription in constructor
11381
+ },
11382
+ });
8501
11383
  // Theme toggle button
8502
11384
  const themeButton = new Button({
8503
11385
  className: 'p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full',
8504
11386
  icon: this.getThemeIcon(),
8505
- title: 'Toggle theme',
11387
+ title: t('theme.toggle'),
8506
11388
  onClick: () => this.handleThemeToggle(),
8507
11389
  });
8508
11390
  // Close button
8509
11391
  const closeButton = new Button({
8510
11392
  className: 'p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full',
8511
11393
  icon: icons.x({ size: 16, color: 'currentColor' }),
8512
- title: 'Close',
11394
+ title: t('app.close'),
8513
11395
  onClick: this.options.onClose,
8514
11396
  });
11397
+ actionsSection.appendChild(this.languageSelector.getElement());
8515
11398
  actionsSection.appendChild(themeButton.getElement());
8516
11399
  actionsSection.appendChild(closeButton.getElement());
8517
11400
  this.headerContainer.appendChild(titleSection);
@@ -8528,16 +11411,20 @@ class PanelRenderer extends BaseComponent {
8528
11411
  this.actionButtonsContainer = document.createElement('div');
8529
11412
  this.actionButtonsContainer.className =
8530
11413
  'flex gap-3 px-6 py-4 border-b border-gray-200 dark:border-gray-700';
11414
+ // Apply RTL direction if needed
11415
+ if (i18n.isRTL()) {
11416
+ this.actionButtonsContainer.setAttribute('dir', 'rtl');
11417
+ }
8531
11418
  // Add Region button
8532
11419
  const addRegionButton = new Button({
8533
- text: 'Add Region',
11420
+ text: t('actions.addRegion'),
8534
11421
  variant: 'primary',
8535
11422
  icon: icons.plus({ size: 16, color: 'white' }),
8536
11423
  onClick: this.options.onAddRegion,
8537
11424
  });
8538
11425
  // Refresh button
8539
11426
  const refreshButton = new Button({
8540
- text: 'Refresh',
11427
+ text: t('actions.refresh'),
8541
11428
  variant: 'secondary',
8542
11429
  icon: icons.refresh({ size: 16, color: 'currentColor' }),
8543
11430
  onClick: () => this.refresh(),
@@ -8609,18 +11496,18 @@ class PanelRenderer extends BaseComponent {
8609
11496
  template: this.createCompleteStyleTemplate(styleWithSize, styleRegions),
8610
11497
  actions: [
8611
11498
  {
8612
- label: 'Load Style',
11499
+ label: t('actions.loadStyle'),
8613
11500
  action: 'load-style',
8614
11501
  icon: icons.cloud({ size: 12, color: 'currentColor' }),
8615
11502
  },
8616
11503
  {
8617
- label: 'Fix Tiles',
11504
+ label: t('actions.fixTiles'),
8618
11505
  action: 'fix-compressed-tiles',
8619
11506
  variant: 'secondary',
8620
11507
  icon: icons.settings({ size: 12, color: 'currentColor' }),
8621
11508
  },
8622
11509
  {
8623
- label: 'Delete Style',
11510
+ label: t('actions.deleteStyle'),
8624
11511
  action: 'delete-style',
8625
11512
  variant: 'danger',
8626
11513
  icon: icons.trash({ size: 12, color: 'currentColor' }),
@@ -8647,12 +11534,12 @@ class PanelRenderer extends BaseComponent {
8647
11534
  template: this.createRegionItemTemplate(region, false),
8648
11535
  actions: [
8649
11536
  {
8650
- label: 'Details',
11537
+ label: t('app.details'),
8651
11538
  action: 'show-details',
8652
11539
  icon: icons.infoCircle({ size: 12, color: 'currentColor' }),
8653
11540
  },
8654
11541
  {
8655
- label: 'Focus',
11542
+ label: t('app.focus'),
8656
11543
  action: 'focus-region',
8657
11544
  icon: icons.focus({ size: 12, color: 'currentColor' }),
8658
11545
  },
@@ -8662,7 +11549,7 @@ class PanelRenderer extends BaseComponent {
8662
11549
  // icon: icons.deviceFloppy({ size: 12, color: 'currentColor' })
8663
11550
  // },
8664
11551
  {
8665
- label: 'Delete',
11552
+ label: t('app.delete'),
8666
11553
  action: 'delete-region',
8667
11554
  variant: 'danger',
8668
11555
  icon: icons.trash({ size: 12, color: 'currentColor' }),
@@ -8675,7 +11562,7 @@ class PanelRenderer extends BaseComponent {
8675
11562
  this.regionsList = new List({
8676
11563
  className: 'flex-1 px-6 py-4 overflow-y-auto',
8677
11564
  items: listItems,
8678
- emptyText: 'No offline styles or regions found. Click "Add Region" to get started.',
11565
+ emptyText: t('regionList.empty'),
8679
11566
  onItemAction: this.handleItemAction.bind(this),
8680
11567
  onItemClick: (itemId, item) => {
8681
11568
  const itemObj = item;
@@ -8713,12 +11600,12 @@ class PanelRenderer extends BaseComponent {
8713
11600
  ${region.name}
8714
11601
  </h4>
8715
11602
  <div class="text-xs text-slate-500 dark:text-slate-400 mt-1">
8716
- ${region.bounds ? `<span class="font-medium">SW:</span> ${region.bounds[0][1].toFixed(4)}, ${region.bounds[0][0].toFixed(4)} <span class="mx-1">→</span> <span class="font-medium">NE:</span> ${region.bounds[1][1].toFixed(4)}, ${region.bounds[1][0].toFixed(4)}` : 'No bounds'}
11603
+ ${region.bounds ? `<span class="font-medium">SW:</span> ${region.bounds[0][1].toFixed(4)}, ${region.bounds[0][0].toFixed(4)} <span class="mx-1">→</span> <span class="font-medium">NE:</span> ${region.bounds[1][1].toFixed(4)}, ${region.bounds[1][0].toFixed(4)}` : t('panel.noBounds')}
8717
11604
  </div>
8718
11605
  <div class="flex items-center gap-4 text-xs text-slate-400 dark:text-slate-500 mt-2">
8719
- <span>Zoom: ${region.minZoom}-${region.maxZoom}</span>
8720
- ${region.downloadedAt ? `<span>Downloaded: ${new Date(region.downloadedAt).toLocaleDateString()}</span>` : ''}
8721
- ${region.size ? `<span>Size: ${formatBytes(region.size)}</span>` : ''}
11606
+ <span>${t('regionList.zoom')}: ${region.minZoom}-${region.maxZoom}</span>
11607
+ ${region.downloadedAt ? `<span>${t('regionList.downloaded')}: ${new Date(region.downloadedAt).toLocaleDateString()}</span>` : ''}
11608
+ ${region.size ? `<span>${t('regionList.size')}: ${formatBytes(region.size)}</span>` : ''}
8722
11609
  </div>
8723
11610
  </div>
8724
11611
  </div>
@@ -8739,16 +11626,16 @@ class PanelRenderer extends BaseComponent {
8739
11626
  <div class="flex items-center justify-between">
8740
11627
  <div class="flex-1">
8741
11628
  <h3 class="font-semibold text-slate-800 dark:text-slate-100 text-base">
8742
- ${style.style?.name || style.key || 'Unnamed Style'}
11629
+ ${style.style?.name || style.key || t('style.unnamedStyle')}
8743
11630
  </h3>
8744
11631
  <p class="text-sm text-slate-600 dark:text-slate-300 mt-1">
8745
- Style ID: ${style.key}
11632
+ ${t('regionList.styleId')}: ${style.key}
8746
11633
  </p>
8747
11634
  <p class="text-sm text-slate-500 dark:text-slate-400 mt-1">
8748
11635
  ${regionCount} region${regionCount === 1 ? '' : 's'} • ${formatBytes(totalSize)}
8749
11636
  </p>
8750
11637
  <p class="text-sm text-slate-500 dark:text-slate-400 mt-1">
8751
- Stored Style Size: ${formatBytes(style.dbSize || 0)}
11638
+ ${t('regionList.storedStyleSize')}: ${formatBytes(style.dbSize || 0)}
8752
11639
  </p>
8753
11640
  </div>
8754
11641
  <div class="flex items-center gap-2">
@@ -8780,28 +11667,28 @@ class PanelRenderer extends BaseComponent {
8780
11667
  ${region.name}
8781
11668
  </h4>
8782
11669
  <div class="text-xs text-slate-500 dark:text-slate-400 mt-1">
8783
- ${region.bounds ? `<span class="font-medium">SW:</span> ${region.bounds[0][1].toFixed(4)}, ${region.bounds[0][0].toFixed(4)} <span class="mx-1">→</span> <span class="font-medium">NE:</span> ${region.bounds[1][1].toFixed(4)}, ${region.bounds[1][0].toFixed(4)}` : 'No bounds'}
11670
+ ${region.bounds ? `<span class="font-medium">SW:</span> ${region.bounds[0][1].toFixed(4)}, ${region.bounds[0][0].toFixed(4)} <span class="mx-1">→</span> <span class="font-medium">NE:</span> ${region.bounds[1][1].toFixed(4)}, ${region.bounds[1][0].toFixed(4)}` : t('panel.noBounds')}
8784
11671
  </div>
8785
11672
  <div class="flex items-center gap-4 text-xs text-slate-400 dark:text-slate-500 mt-2">
8786
- <span>Zoom: ${region.minZoom}-${region.maxZoom}</span>
8787
- ${region.downloadedAt ? `<span>Downloaded: ${new Date(region.downloadedAt).toLocaleDateString()}</span>` : ''}
8788
- ${region.size ? `<span>Size: ${formatBytes(region.size)}</span>` : ''}
11673
+ <span>${t('regionList.zoom')}: ${region.minZoom}-${region.maxZoom}</span>
11674
+ ${region.downloadedAt ? `<span>${t('regionList.downloaded')}: ${new Date(region.downloadedAt).toLocaleDateString()}</span>` : ''}
11675
+ ${region.size ? `<span>${t('regionList.size')}: ${formatBytes(region.size)}</span>` : ''}
8789
11676
  </div>
8790
11677
  </div>
8791
11678
  <div class="flex items-center gap-1 ml-2">
8792
- <button class="region-action-btn p-1.5 rounded-md hover:bg-indigo-100 dark:hover:bg-indigo-900/50 text-indigo-600 dark:text-indigo-400 transition-colors duration-150" data-action="show-details" data-region-id="${region.id}" title="Details">
11679
+ <button class="region-action-btn p-1.5 rounded-md hover:bg-indigo-100 dark:hover:bg-indigo-900/50 text-indigo-600 dark:text-indigo-400 transition-colors duration-150" data-action="show-details" data-region-id="${region.id}" title="${t('app.details')}">
8793
11680
  ${icons.infoCircle({ size: 14, color: 'currentColor' })}
8794
11681
  </button>
8795
- <button class="region-action-btn p-1.5 rounded-md hover:bg-emerald-100 dark:hover:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 transition-colors duration-150" data-action="focus-region" data-region-id="${region.id}" title="Focus">
11682
+ <button class="region-action-btn p-1.5 rounded-md hover:bg-emerald-100 dark:hover:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 transition-colors duration-150" data-action="focus-region" data-region-id="${region.id}" title="${t('app.focus')}">
8796
11683
  ${icons.focus({ size: 14, color: 'currentColor' })}
8797
11684
  </button>
8798
- <button class="region-action-btn p-1.5 rounded-md hover:bg-blue-100 dark:hover:bg-blue-900/50 text-blue-600 dark:text-blue-400 transition-colors duration-150" data-action="redownload-region" data-region-id="${region.id}" title="Re-download">
11685
+ <button class="region-action-btn p-1.5 rounded-md hover:bg-blue-100 dark:hover:bg-blue-900/50 text-blue-600 dark:text-blue-400 transition-colors duration-150" data-action="redownload-region" data-region-id="${region.id}" title="${t('actions.redownload')}">
8799
11686
  ${icons.download({ size: 14, color: 'currentColor' })}
8800
11687
  </button>
8801
- <!-- <button class="region-action-btn p-1.5 rounded-md hover:bg-purple-100 dark:hover:bg-purple-900/50 text-purple-600 dark:text-purple-400 transition-colors duration-150" data-action="import-export" data-region-id="${region.id}" title="Import/Export">
11688
+ <!-- <button class="region-action-btn p-1.5 rounded-md hover:bg-purple-100 dark:hover:bg-purple-900/50 text-purple-600 dark:text-purple-400 transition-colors duration-150" data-action="import-export" data-region-id="${region.id}" title="${t('actions.importExport')}">
8802
11689
  ${icons.deviceFloppy({ size: 14, color: 'currentColor' })}
8803
11690
  </button> -->
8804
- <button class="region-action-btn p-1.5 rounded-md hover:bg-red-100 dark:hover:bg-red-900/50 text-red-600 dark:text-red-400 transition-colors duration-150" data-action="delete-region" data-region-id="${region.id}" title="Delete">
11691
+ <button class="region-action-btn p-1.5 rounded-md hover:bg-red-100 dark:hover:bg-red-900/50 text-red-600 dark:text-red-400 transition-colors duration-150" data-action="delete-region" data-region-id="${region.id}" title="${t('app.delete')}">
8805
11692
  ${icons.trash({ size: 14, color: 'currentColor' })}
8806
11693
  </button>
8807
11694
  </div>
@@ -8822,10 +11709,10 @@ class PanelRenderer extends BaseComponent {
8822
11709
  <div class="flex items-center justify-between">
8823
11710
  <div class="flex-1">
8824
11711
  <h3 class="font-semibold text-blue-900 dark:text-blue-100 text-base">
8825
- ${style.style?.name || style.key || 'Unnamed Style'}
11712
+ ${style.style?.name || style.key || t('style.unnamedStyle')}
8826
11713
  </h3>
8827
11714
  <p class="text-sm text-blue-700 dark:text-blue-300 mt-1">
8828
- Style ID: ${style.key}
11715
+ ${t('regionList.styleId')}: ${style.key}
8829
11716
  </p>
8830
11717
  <p class="text-sm text-blue-600 dark:text-blue-400 mt-1">
8831
11718
  ${regionCount} region${regionCount === 1 ? '' : 's'} • ${formatBytes(totalSize)}
@@ -8855,10 +11742,10 @@ class PanelRenderer extends BaseComponent {
8855
11742
  <div class="flex items-center justify-between">
8856
11743
  <div>
8857
11744
  <h3 class="font-semibold text-amber-800 dark:text-amber-200 text-sm">
8858
- Regions without Style
11745
+ ${t('regionList.orphanedHeader')}
8859
11746
  </h3>
8860
11747
  <p class="text-xs text-amber-700 dark:text-amber-300 mt-1">
8861
- ${regions.length} region${regions.length === 1 ? '' : 's'} not associated with any style
11748
+ ${t('regionList.orphanedDescription', { count: regions.length })}
8862
11749
  </p>
8863
11750
  </div>
8864
11751
  <div class="text-amber-500 dark:text-amber-400">
@@ -8876,7 +11763,7 @@ class PanelRenderer extends BaseComponent {
8876
11763
  return `
8877
11764
  <div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
8878
11765
  <h3 class="text-sm font-medium text-blue-900 dark:text-blue-100 mb-3">
8879
- Active Downloads (${downloadArray.length})
11766
+ ${t('download.activeCount', { count: downloadArray.length })}
8880
11767
  </h3>
8881
11768
  <div class="space-y-2">
8882
11769
  ${downloadArray
@@ -8956,10 +11843,10 @@ class PanelRenderer extends BaseComponent {
8956
11843
  if (!region)
8957
11844
  return;
8958
11845
  const confirmModal = new ConfirmationModal({
8959
- title: 'Delete Region',
8960
- message: `Are you sure you want to delete the region "${region.name}"? This action cannot be undone.`,
8961
- confirmText: 'Delete',
8962
- cancelText: 'Cancel',
11846
+ title: t('delete.regionTitle'),
11847
+ message: t('delete.regionMessage', { name: region.name }),
11848
+ confirmText: t('app.delete'),
11849
+ cancelText: t('app.cancel'),
8963
11850
  onConfirm: async () => {
8964
11851
  try {
8965
11852
  await this.offlineManager.deleteRegion(regionId);
@@ -8995,10 +11882,10 @@ class PanelRenderer extends BaseComponent {
8995
11882
  return;
8996
11883
  }
8997
11884
  const confirmModal = new ConfirmationModal({
8998
- title: 'Re-download Region',
8999
- message: `Re-download "${region.name}"?\n\nThis will:\n• Delete existing tiles and resources\n• Re-download all data with current settings\n• Fix any corrupted or outdated resources`,
9000
- confirmText: 'Re-download',
9001
- cancelText: 'Cancel',
11885
+ title: t('redownload.title'),
11886
+ message: t('redownload.message', { name: region.name }),
11887
+ confirmText: t('redownload.button'),
11888
+ cancelText: t('app.cancel'),
9002
11889
  onConfirm: async () => {
9003
11890
  try {
9004
11891
  this.modalManager.close();
@@ -9026,14 +11913,14 @@ class PanelRenderer extends BaseComponent {
9026
11913
  }
9027
11914
  else {
9028
11915
  panelLogger.error('Download manager not available');
9029
- alert('Download manager not available. Please try again.');
11916
+ alert(t('alert.downloadManagerNotAvailable'));
9030
11917
  }
9031
11918
  // Refresh the panel to show the new region
9032
11919
  await this.refresh();
9033
11920
  }
9034
11921
  catch (error) {
9035
11922
  panelLogger.error('Failed to re-download region:', error);
9036
- alert(`Failed to re-download region: ${error instanceof Error ? error.message : 'Unknown error'}`);
11923
+ alert(`${t('alert.failedToRedownload')}: ${error instanceof Error ? error.message : t('alert.unknownError')}`);
9037
11924
  }
9038
11925
  },
9039
11926
  onCancel: () => {
@@ -9123,10 +12010,10 @@ class PanelRenderer extends BaseComponent {
9123
12010
  */
9124
12011
  renderErrorState(panelElement) {
9125
12012
  panelElement.innerHTML = `
9126
- <div class="flex items-center justify-center h-full text-red-600 dark:text-red-400">
12013
+ <div class="flex items-center justify-center h-full text-red-600 dark:text-red-400" ${i18n.isRTL() ? 'dir="rtl"' : ''}>
9127
12014
  <div class="text-center">
9128
- <p class="text-lg font-medium">Error loading content</p>
9129
- <p class="text-sm mt-2">Please try again</p>
12015
+ <p class="text-lg font-medium">${t('error.loadingContent')}</p>
12016
+ <p class="text-sm mt-2">${t('error.tryAgain')}</p>
9130
12017
  </div>
9131
12018
  </div>
9132
12019
  `;
@@ -9194,12 +12081,12 @@ class PanelRenderer extends BaseComponent {
9194
12081
  template: this.createRegionItemTemplate(region, false),
9195
12082
  actions: [
9196
12083
  {
9197
- label: 'Details',
12084
+ label: t('app.details'),
9198
12085
  action: 'show-details',
9199
12086
  icon: icons.infoCircle({ size: 12, color: 'currentColor' }),
9200
12087
  },
9201
12088
  {
9202
- label: 'Focus',
12089
+ label: t('app.focus'),
9203
12090
  action: 'focus-region',
9204
12091
  icon: icons.focus({ size: 12, color: 'currentColor' }),
9205
12092
  },
@@ -9209,7 +12096,7 @@ class PanelRenderer extends BaseComponent {
9209
12096
  // icon: icons.deviceFloppy({ size: 12, color: 'currentColor' })
9210
12097
  // },
9211
12098
  {
9212
- label: 'Delete',
12099
+ label: t('app.delete'),
9213
12100
  action: 'delete-region',
9214
12101
  variant: 'danger',
9215
12102
  icon: icons.trash({ size: 12, color: 'currentColor' }),
@@ -9220,7 +12107,7 @@ class PanelRenderer extends BaseComponent {
9220
12107
  this.regionsList = new List({
9221
12108
  className: 'flex-1 px-6 py-4 overflow-y-auto',
9222
12109
  items: listItems,
9223
- emptyText: 'No offline regions found. Click "Add Region" to get started.',
12110
+ emptyText: t('regionList.emptyFallback'),
9224
12111
  onItemAction: this.handleItemAction.bind(this),
9225
12112
  onItemClick: (itemId, _item) => {
9226
12113
  this.handleShowRegionDetails(itemId);
@@ -9290,6 +12177,12 @@ class PanelRenderer extends BaseComponent {
9290
12177
  if (this.regionsList) {
9291
12178
  this.regionsList.destroy();
9292
12179
  }
12180
+ if (this.languageSelector) {
12181
+ this.languageSelector.destroy();
12182
+ }
12183
+ if (this.unsubscribeLanguage) {
12184
+ this.unsubscribeLanguage();
12185
+ }
9293
12186
  super.destroy();
9294
12187
  }
9295
12188
  /**
@@ -9346,9 +12239,9 @@ class PanelRenderer extends BaseComponent {
9346
12239
  const stats = await countCompressedTiles();
9347
12240
  if (stats.gzipped === 0) {
9348
12241
  const confirmModal = new ConfirmationModal({
9349
- title: 'No Issues Found',
9350
- message: `No compressed tiles detected. Your tiles are already in the correct format!`,
9351
- confirmText: 'OK',
12242
+ title: t('fixTiles.noIssuesTitle'),
12243
+ message: t('fixTiles.noIssuesMessage'),
12244
+ confirmText: t('app.ok'),
9352
12245
  cancelText: '',
9353
12246
  onConfirm: () => {
9354
12247
  this.modalManager.close();
@@ -9362,10 +12255,10 @@ class PanelRenderer extends BaseComponent {
9362
12255
  return;
9363
12256
  }
9364
12257
  const confirmModal = new ConfirmationModal({
9365
- title: 'Fix Compressed Tiles',
9366
- message: `Found ${stats.gzipped} compressed tiles that may cause rendering errors.\n\nThis will:\n1. Remove all compressed tiles (${stats.gzipped} tiles)\n2. You'll need to re-download regions to get properly decompressed tiles\n\nContinue?`,
9367
- confirmText: 'Fix Tiles',
9368
- cancelText: 'Cancel',
12258
+ title: t('fixTiles.title'),
12259
+ message: t('fixTiles.message', { count: stats.gzipped }),
12260
+ confirmText: t('fixTiles.button'),
12261
+ cancelText: t('app.cancel'),
9369
12262
  onConfirm: async () => {
9370
12263
  try {
9371
12264
  panelLogger.debug('Cleaning up compressed tiles...');
@@ -9374,9 +12267,9 @@ class PanelRenderer extends BaseComponent {
9374
12267
  this.modalManager.close();
9375
12268
  // Show success message
9376
12269
  const successModal = new ConfirmationModal({
9377
- title: 'Cleanup Complete',
9378
- message: `Successfully removed ${result.removed} compressed tiles.\n\nPlease re-download your regions to get the fixed tiles.`,
9379
- confirmText: 'OK',
12270
+ title: t('fixTiles.completeTitle'),
12271
+ message: t('fixTiles.completeMessage', { count: result.removed }),
12272
+ confirmText: t('app.ok'),
9380
12273
  cancelText: '',
9381
12274
  onConfirm: () => {
9382
12275
  this.modalManager.close();
@@ -9412,19 +12305,19 @@ class PanelRenderer extends BaseComponent {
9412
12305
  async handleLoadStyle(styleData) {
9413
12306
  if (!this.map) {
9414
12307
  panelLogger.warn('Map not available for loading style');
9415
- alert('Map is not initialized. Please ensure the map is loaded before loading a style.');
12308
+ alert(t('error.mapNotInitialized'));
9416
12309
  return;
9417
12310
  }
9418
12311
  try {
9419
12312
  const styleEntry = styleData;
9420
12313
  if (!styleEntry || !styleEntry.style) {
9421
12314
  panelLogger.error('Invalid style data - missing style property');
9422
- alert('Invalid style data. The style may be corrupted.');
12315
+ alert(t('error.invalidStyleData'));
9423
12316
  return;
9424
12317
  }
9425
12318
  if (!styleEntry.key) {
9426
12319
  panelLogger.error('Invalid style data - missing key property');
9427
- alert('Invalid style data. The style key is missing.');
12320
+ alert(t('error.invalidStyleData'));
9428
12321
  return;
9429
12322
  }
9430
12323
  panelLogger.debug('Loading style from IndexedDB:', {
@@ -9440,12 +12333,12 @@ class PanelRenderer extends BaseComponent {
9440
12333
  // Validate style structure
9441
12334
  if (!styleEntry.style.sources || Object.keys(styleEntry.style.sources).length === 0) {
9442
12335
  panelLogger.error('Style is missing sources');
9443
- alert('Style has no sources defined. The style may be incomplete.');
12336
+ alert(t('error.styleMissingSources'));
9444
12337
  return;
9445
12338
  }
9446
12339
  if (!styleEntry.style.layers || styleEntry.style.layers.length === 0) {
9447
12340
  panelLogger.error('Style is missing layers');
9448
- alert('Style has no layers defined. The style may be incomplete.');
12341
+ alert(t('error.styleMissingLayers'));
9449
12342
  return;
9450
12343
  }
9451
12344
  // Check for compressed tiles
@@ -9454,9 +12347,9 @@ class PanelRenderer extends BaseComponent {
9454
12347
  const compressed = await countCompressedTiles();
9455
12348
  if (compressed.gzipped > 0) {
9456
12349
  panelLogger.warn(`Found ${compressed.gzipped} compressed tiles that may cause rendering issues`);
9457
- const shouldContinue = confirm(`Warning: Found ${compressed.gzipped} compressed tiles that may cause rendering issues.\n\n` +
9458
- `Recommended: Use the "Re-download" button to fix this issue.\n\n` +
9459
- `Do you want to continue loading the style anyway?`);
12350
+ const shouldContinue = confirm(`${t('warning.compressedTiles', { count: compressed.gzipped })}\n\n` +
12351
+ `${t('warning.compressedTilesRecommendation')}\n\n` +
12352
+ `${t('warning.continueAnyway')}`);
9460
12353
  if (!shouldContinue) {
9461
12354
  return;
9462
12355
  }
@@ -9525,7 +12418,7 @@ class PanelRenderer extends BaseComponent {
9525
12418
  }
9526
12419
  catch (setStyleError) {
9527
12420
  panelLogger.error('Error calling setStyle:', setStyleError);
9528
- alert(`Failed to apply style to map: ${setStyleError instanceof Error ? setStyleError.message : 'Unknown error'}`);
12421
+ alert(`${t('alert.failedToApplyStyle')}: ${setStyleError instanceof Error ? setStyleError.message : t('alert.unknownError')}`);
9529
12422
  return;
9530
12423
  }
9531
12424
  }
@@ -9535,7 +12428,7 @@ class PanelRenderer extends BaseComponent {
9535
12428
  message: error instanceof Error ? error.message : String(error),
9536
12429
  stack: error instanceof Error ? error.stack : undefined,
9537
12430
  });
9538
- alert(`Failed to load style: ${error instanceof Error ? error.message : 'Unknown error'}`);
12431
+ alert(`${t('alert.failedToLoadStyle')}: ${error instanceof Error ? error.message : t('alert.unknownError')}`);
9539
12432
  }
9540
12433
  }
9541
12434
  /**
@@ -9545,10 +12438,10 @@ class PanelRenderer extends BaseComponent {
9545
12438
  try {
9546
12439
  const style = styleData;
9547
12440
  const confirmModal = new ConfirmationModal({
9548
- title: 'Delete Style',
9549
- message: `Are you sure you want to delete the style "${style.style?.name || style.key}"? This action cannot be undone and will affect associated regions.`,
9550
- confirmText: 'Delete Style',
9551
- cancelText: 'Cancel',
12441
+ title: t('delete.styleTitle'),
12442
+ message: t('delete.styleMessage', { name: style.style?.name || style.key }),
12443
+ confirmText: t('actions.deleteStyle'),
12444
+ cancelText: t('app.cancel'),
9552
12445
  onConfirm: async () => {
9553
12446
  try {
9554
12447
  const { deleteStyleById } = await Promise.resolve().then(function () { return styleService; });
@@ -14628,8 +17521,8 @@ class RegionFormModal {
14628
17521
  show() {
14629
17522
  const { bounds, area } = this.options;
14630
17523
  const modalConfig = {
14631
- title: 'Download Offline Region',
14632
- subtitle: `Selected area: ${area} km²`,
17524
+ title: t('regionForm.title'),
17525
+ subtitle: t('regionForm.subtitle', { area: area.toString() }),
14633
17526
  size: 'md',
14634
17527
  closable: true,
14635
17528
  showThemeToggle: true,
@@ -14654,19 +17547,22 @@ class RegionFormModal {
14654
17547
  const styleUrl = this.options.styleUrl;
14655
17548
  const form = document.createElement('div');
14656
17549
  form.className = 'flex flex-col gap-6 py-2';
17550
+ if (i18n.isRTL()) {
17551
+ form.setAttribute('dir', 'rtl');
17552
+ }
14657
17553
  // Region info display (Moved to top for better context)
14658
17554
  const infoGroup = document.createElement('div');
14659
17555
  infoGroup.className =
14660
17556
  'grid grid-cols-2 gap-4 p-4 rounded-xl glass-input border border-gray-100/50 dark:border-gray-700/50 bg-gray-50/50 dark:bg-gray-800/50';
14661
17557
  infoGroup.innerHTML = `
14662
17558
  <div class="flex flex-col justify-center">
14663
- <strong class="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">Area</strong>
17559
+ <strong class="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">${t('regionForm.area')}</strong>
14664
17560
  <div class="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300">
14665
17561
  ${area.toLocaleString()} <span class="text-sm font-medium text-gray-500">km²</span>
14666
17562
  </div>
14667
17563
  </div>
14668
17564
  <div>
14669
- <strong class="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">Bounds</strong>
17565
+ <strong class="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">${t('regionForm.bounds')}</strong>
14670
17566
  <div class="text-xs font-mono text-gray-600 dark:text-gray-300 leading-relaxed bg-white/50 dark:bg-black/20 p-2 rounded-lg">
14671
17567
  ${west.toFixed(4)}, ${south.toFixed(4)}<br>
14672
17568
  ${east.toFixed(4)}, ${north.toFixed(4)}
@@ -14678,12 +17574,12 @@ class RegionFormModal {
14678
17574
  const nameGroup = document.createElement('div');
14679
17575
  nameGroup.innerHTML = `
14680
17576
  <label class="block mb-2 text-sm font-semibold text-gray-700 dark:text-gray-300">
14681
- Region Name
17577
+ ${t('regionForm.name')}
14682
17578
  </label>
14683
17579
  `;
14684
17580
  this.nameInput = document.createElement('input');
14685
17581
  this.nameInput.type = 'text';
14686
- this.nameInput.placeholder = 'e.g., Downtown District';
17582
+ this.nameInput.placeholder = t('regionForm.namePlaceholder');
14687
17583
  this.nameInput.className =
14688
17584
  'w-full px-4 py-3 rounded-xl text-base glass-input text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500/50 transition-all shadow-sm';
14689
17585
  nameGroup.appendChild(this.nameInput);
@@ -14694,7 +17590,7 @@ class RegionFormModal {
14694
17590
  const minZoomDiv = document.createElement('div');
14695
17591
  minZoomDiv.innerHTML = `
14696
17592
  <label class="block mb-2 text-sm font-semibold text-gray-700 dark:text-gray-300">
14697
- Min Zoom
17593
+ ${t('regionForm.minZoom')}
14698
17594
  </label>
14699
17595
  `;
14700
17596
  this.minZoomInput = document.createElement('input');
@@ -14708,7 +17604,7 @@ class RegionFormModal {
14708
17604
  const maxZoomDiv = document.createElement('div');
14709
17605
  maxZoomDiv.innerHTML = `
14710
17606
  <label class="block mb-2 text-sm font-semibold text-gray-700 dark:text-gray-300">
14711
- Max Zoom
17607
+ ${t('regionForm.maxZoom')}
14712
17608
  </label>
14713
17609
  `;
14714
17610
  this.maxZoomInput = document.createElement('input');
@@ -14726,7 +17622,7 @@ class RegionFormModal {
14726
17622
  const styleGroup = document.createElement('div');
14727
17623
  styleGroup.innerHTML = `
14728
17624
  <label class="block mb-2 text-sm font-semibold text-gray-700 dark:text-gray-300">
14729
- Style URL
17625
+ ${t('regionForm.styleUrl')}
14730
17626
  </label>
14731
17627
  `;
14732
17628
  this.styleUrlInput = document.createElement('input');
@@ -14741,16 +17637,16 @@ class RegionFormModal {
14741
17637
  const providerGroup = document.createElement('div');
14742
17638
  providerGroup.innerHTML = `
14743
17639
  <label class="block mb-2 text-sm font-semibold text-gray-700 dark:text-gray-300">
14744
- Style Provider
17640
+ ${t('regionForm.provider')}
14745
17641
  </label>
14746
17642
  `;
14747
17643
  this.providerSelect = document.createElement('select');
14748
17644
  this.providerSelect.className =
14749
17645
  'w-full px-4 py-3 rounded-xl text-base glass-input text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500/50 transition-all appearance-none cursor-pointer shadow-sm';
14750
17646
  this.providerSelect.innerHTML = `
14751
- <option value="auto">Auto-detect</option>
14752
- <option value="mapbox">Mapbox GL (requires access token)</option>
14753
- <option value="maplibre">MapLibre GL (open source)</option>
17647
+ <option value="auto">${t('regionForm.providerAuto')}</option>
17648
+ <option value="mapbox">${t('regionForm.providerMapbox')}</option>
17649
+ <option value="maplibre">${t('regionForm.providerMaplibre')}</option>
14754
17650
  `;
14755
17651
  this.providerSelect.addEventListener('change', () => this.handleProviderChange());
14756
17652
  providerGroup.appendChild(this.providerSelect);
@@ -14758,7 +17654,7 @@ class RegionFormModal {
14758
17654
  const providerInfo = document.createElement('div');
14759
17655
  providerInfo.className =
14760
17656
  'text-xs text-gray-500 dark:text-gray-400 mt-2 ml-1 flex items-center gap-1';
14761
- providerInfo.innerHTML = `${icons.infoCircle({ size: 12 })} Auto-detect will analyze the style URL to determine the provider`;
17657
+ providerInfo.innerHTML = `${icons.infoCircle({ size: 12 })} ${t('regionForm.providerInfo')}`;
14762
17658
  providerGroup.appendChild(providerInfo);
14763
17659
  form.appendChild(providerGroup);
14764
17660
  // Access Token input
@@ -14766,8 +17662,8 @@ class RegionFormModal {
14766
17662
  this.accessTokenGroup.className = 'hidden';
14767
17663
  this.accessTokenGroup.innerHTML = `
14768
17664
  <label class="block mb-2 text-sm font-semibold text-gray-700 dark:text-gray-300">
14769
- Mapbox Access Token
14770
- <span class="ml-2 text-xs font-normal text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/30 px-2 py-0.5 rounded-full border border-amber-200 dark:border-amber-800">Required</span>
17665
+ ${t('regionForm.accessToken')}
17666
+ <span class="ml-2 text-xs font-normal text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/30 px-2 py-0.5 rounded-full border border-amber-200 dark:border-amber-800">${t('regionForm.accessTokenRequired')}</span>
14771
17667
  </label>
14772
17668
  `;
14773
17669
  this.accessTokenInput = document.createElement('input');
@@ -14780,7 +17676,7 @@ class RegionFormModal {
14780
17676
  const tokenHelp = document.createElement('div');
14781
17677
  tokenHelp.className = 'text-xs text-gray-500 dark:text-gray-400 mt-2 ml-1';
14782
17678
  tokenHelp.innerHTML = `
14783
- Get your access token from <a href="https://account.mapbox.com/access-tokens/" target="_blank" rel="noopener noreferrer" class="text-primary-600 dark:text-primary-400 hover:underline font-medium">Mapbox Account</a>
17679
+ ${t('regionForm.accessTokenHelp')} <a href="https://account.mapbox.com/access-tokens/" target="_blank" rel="noopener noreferrer" class="text-primary-600 dark:text-primary-400 hover:underline font-medium">Mapbox Account</a>
14784
17680
  `;
14785
17681
  this.accessTokenGroup.appendChild(tokenHelp);
14786
17682
  form.appendChild(this.accessTokenGroup);
@@ -14844,9 +17740,12 @@ class RegionFormModal {
14844
17740
  createFooter() {
14845
17741
  const footer = document.createElement('div');
14846
17742
  footer.className = 'flex gap-2 justify-end';
17743
+ if (i18n.isRTL()) {
17744
+ footer.setAttribute('dir', 'rtl');
17745
+ }
14847
17746
  // Cancel button
14848
17747
  const cancelButton = new Button({
14849
- text: 'Cancel',
17748
+ text: t('app.cancel'),
14850
17749
  variant: 'secondary',
14851
17750
  onClick: () => {
14852
17751
  this.modal?.hide();
@@ -14855,7 +17754,7 @@ class RegionFormModal {
14855
17754
  });
14856
17755
  // Save button
14857
17756
  const saveButton = new Button({
14858
- text: 'Download Region',
17757
+ text: t('regionForm.downloadRegion'),
14859
17758
  variant: 'primary',
14860
17759
  icon: icons.download({ size: 16, color: 'white' }),
14861
17760
  onClick: () => this.handleSave(),
@@ -15836,6 +18735,15 @@ class OfflineManagerControl {
15836
18735
  if (this.regionControl?.isSelectionActive()) {
15837
18736
  this.regionControl.cancelSelection();
15838
18737
  }
18738
+ // Apply RTL direction if needed
18739
+ if (i18n.isRTL()) {
18740
+ this.panel.setAttribute('dir', 'rtl');
18741
+ this.panel.classList.add('rtl');
18742
+ }
18743
+ else {
18744
+ this.panel.setAttribute('dir', 'ltr');
18745
+ this.panel.classList.remove('rtl');
18746
+ }
15839
18747
  this.panel.classList.remove('hidden');
15840
18748
  this.renderPanel();
15841
18749
  }
@@ -16289,5 +19197,5 @@ class OfflineManagerControl {
16289
19197
  }
16290
19198
  }
16291
19199
 
16292
- export { AnalyticsService, CONTENT_TYPES, CategorizedError, CleanupService, DB_NAME, DB_VERSION, DOWNLOAD_DEFAULTS, ERROR_MESSAGES, ErrorType, FontService, GLYPH_CONFIG, GZIP_MAGIC_BYTES, GlyphService, ImportExportService, LogLevel, MAP_PROVIDERS, MaintenanceService, OfflineManagerControl, OfflineMapManager, RESOURCE_TYPES, RegionService, ResourceService, STORAGE_CONFIG, STORE_NAMES, STYLE_CONFIG, SUCCESS_MESSAGES, ScopedLogger, SpriteService, TILE_CONFIG, TileService, URL_SCHEMES, VALIDATION_PATTERNS, aggregateErrors, applyProxy, calculateBBoxArea, categorizeError, cleanupCompressedTiles, cleanupOldFonts, cleanupOldGlyphs, cleanupOldSprites, cleanupOldStyles, cleanupOldTiles, cleanupService, clearAllCaches, clearRegionStyleCache, configureLogger, configureProxy, countCompressedTiles, createProgressTracker, createTileKey, dbPromise, OfflineMapManager as default, deleteStyleById, deleteStyles, deriveTileExtension, detectStyleProvider, downloadFonts, downloadGlyphs, downloadSprites, downloadStyleWithProvider, downloadStyles, downloadTiles, estimateTileCount, extractAccessToken, extractTileKey, fetchResource, fetchResourceWithRetry, fetchWithRetry, fontService, formatBytes, formatDate, formatDateTime, formatDuration, formatNumber, formatPercentage, generateGlyphUrlsFromStyle, getDefaultStyleConfig, getFontAnalytics, getFontStats, getGlyphAnalytics, getGlyphStats, getIcon, getProxyConfig, getRegionAnalytics, getSpriteAnalytics, getSpriteStats, getStyleStats, getTileAnalytics, getTileStats, getUserErrorMessage, glyphService, icons, idbFetchHandler, isCorsError, isRetryableError, isStyleDownloaded, listTileKeysInIDB, loadGlyphs, loadStyleById, loadStyles, logError, logger, normalizeStyleUrl, optimizeStorage, parseTileKey, patchStyleForOffline, performCleanup, processBatch, processStyleSources, requiresAuthentication, resetProxyConfig, safeExecute, safeValidate, setupAutoCleanup, shouldProxy, spriteService, stopAutoCleanup, tileService, validateBatchSize, validateBounds, validateRegion, validateRegionId, validateRegionOptions, validateResource, validateStyleForProvider, validateStyleUrl, validateTimeout, validateZoomLevels, verifyAndRepairFonts, verifyAndRepairGlyphs, verifyAndRepairSprites };
19200
+ export { AnalyticsService, CONTENT_TYPES, CategorizedError, CleanupService, DB_NAME, DB_VERSION, DOWNLOAD_DEFAULTS, ERROR_MESSAGES, ErrorType, FontService, GLYPH_CONFIG, GZIP_MAGIC_BYTES, GlyphService, ImportExportService, LogLevel, MAP_PROVIDERS, MaintenanceService, OfflineManagerControl, OfflineMapManager, RESOURCE_TYPES, RegionService, ResourceService, STORAGE_CONFIG, STORE_NAMES, STYLE_CONFIG, SUCCESS_MESSAGES, ScopedLogger, SpriteService, TILE_CONFIG, TileService, URL_SCHEMES, VALIDATION_PATTERNS, aggregateErrors, applyProxy, calculateBBoxArea, categorizeError, cleanupCompressedTiles, cleanupOldFonts, cleanupOldGlyphs, cleanupOldSprites, cleanupOldStyles, cleanupOldTiles, cleanupService, clearAllCaches, clearRegionStyleCache, configureLogger, configureProxy, countCompressedTiles, createProgressTracker, createTileKey, dbPromise, OfflineMapManager as default, deleteStyleById, deleteStyles, deriveTileExtension, detectStyleProvider, downloadFonts, downloadGlyphs, downloadSprites, downloadStyleWithProvider, downloadStyles, downloadTiles, estimateTileCount, extractAccessToken, extractTileKey, fetchResource, fetchResourceWithRetry, fetchWithRetry, fontService, formatBytes, formatDate, formatDateTime, formatDuration, formatNumber, formatPercentage, generateGlyphUrlsFromStyle, getDefaultStyleConfig, getFontAnalytics, getFontStats, getGlyphAnalytics, getGlyphStats, getIcon, getProxyConfig, getRegionAnalytics, getSpriteAnalytics, getSpriteStats, getStyleStats, getTileAnalytics, getTileStats, getUserErrorMessage, glyphService, i18n, icons, idbFetchHandler, isCorsError, isRetryableError, isStyleDownloaded, listTileKeysInIDB, loadGlyphs, loadStyleById, loadStyles, logError, logger, normalizeStyleUrl, optimizeStorage, parseTileKey, patchStyleForOffline, performCleanup, processBatch, processStyleSources, requiresAuthentication, resetProxyConfig, safeExecute, safeValidate, setupAutoCleanup, shouldProxy, spriteService, stopAutoCleanup, t, tileService, validateBatchSize, validateBounds, validateRegion, validateRegionId, validateRegionOptions, validateResource, validateStyleForProvider, validateStyleUrl, validateTimeout, validateZoomLevels, verifyAndRepairFonts, verifyAndRepairGlyphs, verifyAndRepairSprites };
16293
19201
  //# sourceMappingURL=index.esm.js.map